agentgateway is an open source gateway you run yourself, and it covers the jobs most teams use Portkey for: virtual keys that hide the provider key, per-key budgets in dollars or tokens, guardrails on prompts and responses, and CEL policy over which MCP tools an agent can see and call. It is Apache 2.0 and lives under the Linux Foundation. Run it as one binary with a YAML file, or on Kubernetes with Gateway API. I ran the virtual key, budget, MCP and SDK examples on this page on v1.6.0 on 2026-10-26, and the outputs below are from those runs. The guardrail section is from the docs.
If you want LiteLLM and Portkey side by side, that is on my comparison page .
What agentgateway is
agentgateway is one data plane for HTTP, gRPC, LLM, MCP and A2A traffic. It is written in Rust, licensed Apache 2.0, was donated to the Linux Foundation in 2025, and became an Agentic AI Foundation (AAIF) project in 2026.
It has two modes, and you choose based on where you run things:
| Standalone | Kubernetes | |
|---|---|---|
| Install | A binary , a Docker container, or a single-Deployment Helm chart | Two Helm charts: CRDs plus the control plane |
| Config | One YAML file. The UI can edit it | Custom resources plus Gateway API. The UI is read-only |
| Storage | Optional database. Per-key budgets require one (SQLite or PostgreSQL) | Token budgets use an external rate-limit service and Redis |
| Per-key budgets | USD or tokens | Tokens (open source). USD needs Solo Enterprise |
Every test on this page ran on the open source build. No Solo Enterprise chart, image or license was used.
Concept map
Here is what each Portkey control looks like in agentgateway config.
| Portkey concept | agentgateway standalone | agentgateway on Kubernetes |
|---|---|---|
| Virtual key | llm.policies.apiKey.keys[], provider key in params.apiKey | ConfigMap of hashed keys plus apiKeyAuthentication, provider key in a Secret |
| Budget limit | budgets[] on a key, unit: USD or unit: Tokens | rateLimit.global with unit: Tokens |
| Model access | allowedModels on a key | A CEL authorization rule on the requested model |
| Guardrails | promptGuard request and response guards | promptGuard on an AgentgatewayPolicy |
| MCP authorization | mcpAuthorization CEL rules down to the tool | CEL rules on an AgentgatewayPolicy
, down to the tool |
| Gateway endpoint | http://localhost:4000/v1 | Your Gateway’s address |
Virtual keys and per-key budgets in standalone mode
This is the whole config I ran. Each client gets its own key. The real provider key lives only in the gateway.
config:
database:
url: sqlite://budgets.db
llm:
port: 4000
policies:
apiKey:
mode: strict
keys:
- key: sk-team-a-abc123def456
metadata:
name: team-a
allowedModels:
- "gpt-4o*"
budgets:
- name: daily-spend
limit:
unit: USD
amount: 0.10
window:
rolling: 24h
onBudgetExceeded: Block
- key: sk-team-b-xyz789uvw012
metadata:
name: team-b
budgets:
- name: daily-tokens
limit:
unit: Tokens
amount: 250000
window:
rolling: 24h
onBudgetExceeded: Block
models:
- name: "*"
provider: openAI
params:
apiKey: "sk-provider-real-key-held-by-gateway"
# my local mock; drop baseUrl to reach OpenAI
baseUrl: "http://127.0.0.1:18080/v1"
There is no price table in that file. Since v1.6.0 , a built-in catalog prices common public models. My mock reported 100,000 input and 50,000 output tokens per call on gpt-4o-mini, and the access log priced each call on its own:
gen_ai.request.model=gpt-4o-mini gen_ai.usage.input_tokens=100000 gen_ai.usage.output_tokens=50000 agw.ai.usage.cost.total=0.0450
Calls one to three returned 200. The fourth was blocked before it left the gateway:
HTTP 429
{"error":{"message":"Budget exceeded","type":"rate_limit_error","code":"budget_exceeded"}}
The admin API showed "used":"0.135" against a limit of 0.1. The budget is charged after each response, so the call that crosses the line still completes. Plan for one request of overshoot. The per-key budget docs
describe this, plus an Audit action that logs the overage without blocking.
The same key asking for a model outside gpt-4o* got a 403 before any provider call:
HTTP 403
{"error":{"message":"Model is not allowed for this API key","type":"invalid_request_error","code":"model_not_allowed"}}
The provider only ever saw Authorization: Bearer sk-provider-real-key-held-by-gateway. Clients never hold that key. I wrote up the v1.5.0 version of this demo in per-key LLM budgets that return 429
.
Virtual keys and token budgets on Kubernetes
On Kubernetes, I installed the open source agentgateway-crds and agentgateway Helm charts, v1.6.0, with Gateway API v1.6.0, on a local k3s cluster. Virtual keys are stored as hashes in a labeled ConfigMap, and the provider key sits in a Secret:
apiVersion: v1
kind: ConfigMap
metadata:
name: llm-api-keys
namespace: agentgateway-system
labels: {agentgateway.dev/apikey: "true"}
data:
alice: '{"keyHash": "sha256:84d589f2439e4d9ff4438aa840e63ffcbf231a87e7fef929f975d2bde03dc78c", "metadata": {"user_id": "alice"}}'
bob: '{"keyHash": "sha256:8373319fb10e2e023343d98ade630529f49c53f567cbc6626d09aaf650b2e58c", "metadata": {"user_id": "bob"}}'
---
apiVersion: agentgateway.dev/v1alpha1
kind: AgentgatewayPolicy
metadata: {name: api-key-auth, namespace: agentgateway-system}
spec:
targetRefs: [{group: gateway.networking.k8s.io, kind: Gateway, name: agentgateway-proxy}]
traffic:
apiKeyAuthentication:
mode: Strict
configMapSelector: {matchLabels: {agentgateway.dev/apikey: "true"}}
rateLimit:
global:
domain: agentgateway
backendRef: {kind: Service, name: ratelimit, namespace: ratelimit, port: 8081}
descriptors:
- entries: [{name: user_id, expression: 'apiKey.user_id'}]
unit: Tokens
The budget itself is a global rate limit counted in tokens, served by an external rate-limit service backed by Redis. I gave each user 100 tokens a day, and the mock reported 19 tokens per call. An unknown key got 401. Alice’s first six calls returned 200, and her seventh and eighth were refused:
HTTP/1.1 429 Too Many Requests
x-ratelimit-limit: 100
x-ratelimit-remaining: 0
retry-after: 26228
content-length: 0
Bob, on his own budget, still got 200. Note the difference from standalone: this 429 has an empty body, and the signal is in the headers. Open source Kubernetes budgets count tokens. Dollar budgets on Kubernetes need Solo Enterprise for agentgateway, which is what my team spend caps with virtual keys demo used.
Guardrails
Guards run on the request and the response. The standalone guardrail docs cover regex guards with built-in detectors and custom patterns, OpenAI Moderation, AWS Bedrock Guardrails, Google Model Armor, and webhooks to your own service. Regex guards can mask, reject or audit. Guards run in sequence, so a cheap regex check can run before a remote one. The same guards exist on Kubernetes .
One change in v1.6.0 matters if you stream: a guard provider that fails while checking a streamed response now rejects the content. Set failureMode: failOpen on the guard if you want the old behavior. My walkthrough is in agentgateway guardrails
.
CEL policy for MCP tools
agentgateway authorization is CEL. For MCP, a rule list
decides which tools a client can call, and tools that are not allowed are filtered out of tools/list, so the client never sees them. This is the config I ran against the MCP “everything” test server:
mcp:
port: 3000
policies:
mcpAuthorization:
rules:
- 'mcp.tool.name == "echo"'
targets:
- name: everything
stdio:
cmd: npx
args: ["-y", "@modelcontextprotocol/server-everything"]
Without the rule, tools/list returned 13 tools. With it, the list was ['echo']. Calling echo worked. Calling get-sum returned:
{"jsonrpc":"2.0","id":11,"error":{"code":-32602,"message":"Unknown tool: get-sum"}}
Rules can also match JWT claims, so one tool can be open to everyone and another limited to one user. CEL has sharp edges worth reading about first: CEL authorization gotchas (tested on Solo Enterprise).
For the identity side of who is calling, see SPIFFE identity for AI agents .
Where Portkey’s gateway stands today (from its docs)
Per Portkey’s docs, checked 2026-10-26:
- The docs index is titled “Portkey (Prisma AIRS AI Gateway) Docs”.
- The hybrid architecture splits a Data Plane you run from a Management Plane that holds the dashboard, backend, MySQL, ClickHouse, Redis and blob store. The docs say “No prompt or response data is forwarded to the Management Plane directly.” A self-hosted gateway is registered with Prisma AIRS AI Gateway .
- Budget limits are set per workspace on an integration, with a minimum of $1 or 100 tokens, and reset weekly, monthly or never. Once set, a budget limit “cannot be edited”. Budgets and rate limits can also be set on API keys .
- The error codes list HTTP 412 for an exhausted budget and 446 for failed guardrail checks.
- MCP authorization is available at the workspace and MCP server levels. Tool-level authorization is listed as “Coming soon”.
- The Portkey-AI/gateway
repo is MIT licensed and starts with
npx @portkey-ai/gateway. Its older docs still link the project at github.com/portkey-ai/rubeus, and that URL now redirects to Portkey-AI/gateway.
Migration checklist
- List every Portkey virtual key, its owner, its budget and its allowed models.
- Write one
apiKey.keysentry per client in standalone, or one hashed ConfigMap entry per user on Kubernetes. - Move provider keys into the gateway config or a Kubernetes Secret. Clients get only virtual keys.
- Recreate budgets:
unit: USDorunit: Tokensin standalone,unit: Tokenson Kubernetes. Plan for one request of overshoot. - Recreate guardrails and MCP tool rules, then test a blocked request for each.
- Point each client at the gateway. For the OpenAI Python SDK, the whole change from a stock client in my test was this:
client = OpenAI(base_url="http://localhost:4000/v1", api_key="<virtual key>")
The call returned 200 with the model’s reply and usage, and the upstream saw only the gateway’s provider key. Test any Portkey-specific headers or SDK features your app uses before cutover.
- Watch for 429
budget_exceededand 403model_not_allowedin standalone, and for 429 withx-ratelimit-remaining: 0on Kubernetes. Portkey’s docs list 412 for an exhausted budget, so update any client code that checks for it.
There is no Portkey importer. The agentgateway config importer reads LiteLLM proxy configs only, so a Portkey setup is moved by hand with the concept map above.