Skip to main content

Research: Centralise LLM Routing, Quotas and Budgets

Decision 1 — Bring your own gateway; contracts instead of a product​

  • Gateways agree on inference (OpenAI-compatible) but differ in administration (auth, budgets, usage).
  • So CAIPE fixes the data-plane contracts ①–④ and wraps the control plane ⑤ in a capability-flagged adapter.
LiteLLMAgentGateway (agentgateway.dev)Kong AI GatewayAgentRouter (Envoy AI Gateway)
① OpenAI wirenativeOpenAI-compatible LLM backendsai-proxy (OpenAI format)native; also /v1/embeddings, /anthropic/v1/messages
② JWT validationenable_jwt_auth, user_id_jwt_field, team_id_jwt_fieldlistener jwtAuth (already used for MCP)openid-connect → consumer groupsEnvoy Gateway SecurityPolicy JWT (JWKS); claims copied to headers
Budgets / quotasmax_budget, tpm/rpm per key, user and teamtoken and $ budget limits keyed on claimsai-rate-limiting-advanced per consumer or grouptoken limits (not $) keyed on claim headers × model, CEL cost; QuotaPolicy per backend
⑤ Admin APIREST /key, /team, /spendconfig / K8s policyAdmin API / decKnone; Kubernetes CRDs only
Usage/spend, PrometheusOTelPrometheus / OTelOTel GenAI metrics, Prometheus
ext_authz calloutcustom auth hooknative extAuthzpluginnative (Envoy ext_authz)
  • Licence tiers for the JWT and budget features in LiteLLM and Kong must be confirmed per deployment. Where they're missing, use key auth (Decision 6).
  • AgentRouter is Envoy AI Gateway, renamed and donated to the Agentic AI Foundation. It is not the agentgateway CAIPE already runs for MCP. It is pre-1.0 (v1alpha CRDs).
  • AgentRouter caveats:
    • Budgets are tokens, with no dollar ledger.
    • Usage is charged when the response ends, so a stream can overshoot.
    • QuotaPolicy fails open by default. Set quotaRateLimitFailureModeDeny=true.
    • Needs Redis and a rate-limit service.
    • Limits are CRDs, so its adapter writes through the Kubernetes API or stays none.
  • Sources:

Decision 2 — Code scope: who calls an LLM today​

#CallerCodeModel chosen by
1Agent and subagentsdynamic_agents/services/agent_runtime.py → services/llm_clients.get_llmllm_models
2Agent middlewaredynamic_agents/services/middleware.py → services/llm.get_configured_llmmiddleware params
3AI-assist /suggestdynamic_agents/routes/assistant.py (provider from the request)llm_models (UI picks)
4RAG embeddingsknowledge_bases/rag/common/src/common/embeddings_factory.pyenv
5RAG ontology agentknowledge_bases/rag/agent_ontology/src/agent_ontology/agent.pyenv
  • Paths 1–3 all build models through build_chat_model in ai_platform_engineering/llm_wrapper/src/llm_wrapper/build.py. The switch, the provider override and the auth hook live there, so no caller can bypass them.
  • Path 3 generates text without executing an agent, including before a new agent is saved. It uses a dedicated, provisioned AI-assist system identity for agent attribution while preserving the authenticated user for user attribution. Its owning team pays; the draft or edited agent does not determine billing or authorization. See AI-assist system identity.
  • Bots, autonomous agents, the scheduler and the UI have no LLM client of their own. They inherit through dynamic agents.

Decision 3 — Rollout: phased build, one global switch​

Single releasePhased (chosen)
Blast radiusevery LLM call and RAG ingestone call path per phase
Test matrix5 paths × N gateways1 path × N gateways
Rollbackrevert the releasedisable gateway routing globally; revert deployment/code changes for an individual phase
Switch optionProsCons
Per-model opt-inper-agent canarybudgets can be bypassed via direct models
Global switch (chosen)uniform enforcementneeds gateway aliases = existing model_id
Per-model, then enforcegradual, then stricttwo mechanisms

The phases are delivery stages, with no phase-specific switches. LLM_GATEWAY_ENABLED=false bypasses the gateway for every migrated path even when its endpoint is configured and reachable; direct-provider configuration and credentials must remain available. With routing enabled, gateway failures fail closed. Individual phase rollback requires reverting deployment or code changes while respecting dependencies.

Decision 4 — Identity facts today​

DimensionWhere it livesIn the access token?
usersub✓
service accountpreferred_username = service-account-*✓
agentHMAC header X-CAIPE-Agent-Context (dynamic_agents/services/mcp_client.py)✗
teamMongo / OpenFGA✗
organizationsingle-valued org claim; one tenant per deployment✓
  • Autonomous runs mint a short-lived owner bearer through token exchange (autonomous_agents/.../dynamic_agents_client.py). X-User-Context still carries attribution; dynamic agents stop trusting it with #1753.
  • Dynamic agents already request the CAS action use on an agent, which maps to the OpenFGA relation agent#can_use, at the start of each turn (dynamic_agents/auth/authz.py require_agent_use_permission), for interactive and autonomous runs.

Decision 5 — Which team pays: T1, the agent's owning team​

RuleProsCons
T1 Owning team (chosen)owner_team_slug already exists; deterministic in UI, bots and autonomous runsshared agents bill their owner; mitigated by the per-user limit
T2 Caller's context teamfair allocationneeds an "active team" concept and a UI selector
T3 Caller's home teamstableneeds a "primary" designation
T4 All of the caller's teamsstrictdouble-counts
T5 Hybridfair for global agentstwo rules
  • T1 makes team_id a constant per agent. That is what allows Decision 7.
  • Agents with no owning team use LLM_GATEWAY_DEFAULT_TEAM (platform).

Decision 6 — How identity reaches the gateway: a short-lived JWT, with key auth as fallback​

OptionUserAgentTeamAny gatewayVerdict
A. Sync teams into Keycloak groups✓✗✓JWT gateways✗ no agent; breaks T1
B. Keycloak plugin (SPI) calls CAIPE✓✓✓JWT gateways✗ Java plugin to own
C. Short-lived gateway JWT✓✓✓JWT gatewayschosen (LLM_GATEWAY_AUTH=jwt)
D. Keycloak JWT + HMAC header✓~~AgentGateway only✗ forgeable elsewhere
E. Per-agent gateway keys~✓✓allfallback (LLM_GATEWAY_AUTH=key); user unverified
G. Gateway calls CAIPE per request✓✓✓per-gateway glueoptional layer, not the base

Identity option legend​

These labels name design options used below and in the companion specification documents.

LabelMeaning
K1Keycloak signs using dynamic scopes supplied with the token request.
K2Keycloak signs using a client scope per agent, maintained by CAIPE with the agent ID and owning team. Preferred option, subject to spike S1.
K3Keycloak signs using a custom Service Provider Interface (SPI) plugin that queries CAIPE for identity context.
C1CAIPE's dynamic-agents service signs the gateway JWT using its own signing key. Fallback if spike S1 rules out K2.
GThe gateway calls CAIPE for identity or authorization on each request instead of relying on a new gateway JWT.

First-call onboarding (option C)

Decision 7 — Who signs: Keycloak per-agent scope (K2), fallback C1​

  • The signer attests. CAIPE (OpenFGA + Mongo) decides.
  • The gateway cannot be the signer: it would be vouching to itself.
K1 dynamic scopesK2 per-agent scopeK3 SPIC1 dynamic agents signG callout
Issuers11120
New signing key in CAIPEnononoyesno
Who picks agent_iddynamic agentsdynamic agents (any scope its client holds)Keycloak via CAIPEdynamic agentsCAIPE
team_id sourcerequesteradmin-synced scopeCAIPEMongoCAIPE
Team change takes effectinstantafter syncinstantinstantinstant
On the hot pathKeycloakKeycloak (on cache miss)Keycloak + CAIPEnoneCAIPE every call
Riskpreview featurespike S1custom codekey managementper-gateway glue
  • K2 and C1 trust dynamic agents equally to pick the agent. K2's gain is no new signing key and Keycloak's audit trail; team_id is fixed by the admin-synced scope.
  • Ownership synchronization can partially fail: a Keycloak scope update may succeed while the MongoDB ownership write fails. Reversing the write order only reverses the possible mismatch.
  • MongoDB ownership remains authoritative. Transfers must be recoverable; CAIPE blocks new calls, including cached-credential calls, while a transfer is unresolved and checks credential bindings before use. Completion invalidates cached credentials for the previous owner; already-admitted calls may finish under that owner.
  • JWT expiry bounds the lifetime of an issued token, not a persistent synchronization failure: an unrepaired scope can keep issuing tokens for the wrong team. Gateway JWT validation remains local. The same ownership consistency policy applies to C1 signing and per-agent gateway key bindings as applicable.
  • #2901 adds operator-owned per-agent Keycloak clients for agent badges. If it lands first, K2 reuses its per-agent binding instead of adding a second per-agent Keycloak object.

K2 flow

Spike S1 (gates phase 1)

  • Keycloak 26.3 exchange emits the mappers of the requested optional scope.
  • Autonomous runs get the scope through the mechanism #2890 selects (actor-preserving exchange, or a service token plus a trusted run binding), without depending on requested_subject impersonation.
  • Token issuance latency and admin performance with 1k, 5k and 10k client scopes.
  • If any check fails → C1 (dynamic agents sign; key optionally in Vault or a cloud KMS).

Decision 8 — Data and state​

DataSource of truthNew?
Model catalogueMongo llm_modelsno
Limits / budgetsgateway, keyed by org + principalno CAIPE copy
Usage / spendgateway (read live); history via OTel/Prometheusno CAIPE copy
Model and agent permissionsOpenFGAno
Agent → owning teamagent document owner_team_slugno
Gateway identity claimsKeycloak client scope per agentyes (K2)
Per-agent gateway keysCAIPE credentials store (ui/src/lib/credentials/)only with key auth
Quota requestsMongo llm_quota_requestsyes

Decision 9 — Authorization at call time​

  • Runtime (always): dynamic agents use the CAS access API (#2889) to check OpenFGA relations agent#can_use (CAS action use, already enforced) and llm_model#can_read (runtime check to add) once per turn per (user, agent, model). Every LLM call in that turn reuses the decision.
  • Gateway (optional): where ext_authz exists, the gateway asks the BFF CAS adapter (#2892, #2909), as AgentGateway does for MCP, for the same decision and verified identity headers.

Decision 10 — Budget errors without gateway code in agents​

  • Named adapters ship documented declarative budget-signal rules: status codes plus a gateway header, structured error code or unambiguous pattern identifying a gateway-enforced limit, and where to read scope and reset time. Forwarded provider errors must remain distinguishable. Status codes or "budget"/"quota" keywords alone cannot establish the source of a rejection.
  • The rule is data in llm_wrapper, overridable with LLM_GATEWAY_BUDGET_SIGNAL. Agents contain no per-gateway code.
  • Adapter none has no default rule. Operators configure LLM_GATEWAY_BUDGET_SIGNAL for their gateway before budget-error conformance can pass.
  • Unmatched or ambiguous errors keep their original status and classification without inferred budget scope or budget-increase advice. Provider quota errors may use the same status codes and words as gateway limits; raising a gateway budget would not resolve them.

Decision 11 — Availability and latency​

  • Fail closed. HA belongs to the gateway operator.
  • Provider 5xx, 429 and timeouts pass through with their class preserved. No silent retry by CAIPE.
  • No latency SLO. Measure and document the added hop with a cached token.

Checked 2026-10-09; no duplicate found.

ItemRelation
#1753 JWT-only identity in dynamic agentsDependency of phase 1: the gateway token needs a verified user
#934 RFC 8693 token exchange and impersonationSame mechanism; prior art for K2
#974, #973 dynamic LLM keys, LiteLLM keysAnswered by Decisions 6–7
#2610 tag LiteLLM requests by agentBecomes the LiteLLM mapping of ③
#2019 FinOps alerting on LiteLLM usageConsumes OTel GenAI usage
#2300 wire setup-caipe to LiteLLMRe-scope to the dev/demo example gateway (T801)
#2813, #2841, #2828, #2854 CAS authorizationllm_model joins the CAS scope and its audit
#2884 CAS migration inventoryThe llm_model call-time check is registered as a CAS path
#2885 canonical platform identityGateway token claims (sub, act, org) follow it
#2889 Dynamic Agents on the CAS access APIPath for the llm_model#can_read check
#2890 OBO impersonation and delegation scopeDecides the autonomous exchange mechanism (spike S1)
#2891 trusted execution context for delegationSource of the task owner for autonomous runs
#2892, #2909 AgentGateway ext_authz through BFF CASPattern for optional gateway-side authorization
#2901 Keycloak-authenticated agent badgesPer-agent Keycloak binding that K2 can reuse
#2883 ACP native runtime (draft)May move dynamic_agents call sites; the hook lives in llm_wrapper
#1037, #1048 quota-increase requestsThe user journey behind T502
#549 LLM fallback on throttlingThe gateway's job

Prior art​

  • setup-caipe.sh deploy_litellm: a dev/demo LiteLLM path; documented as one example gateway.
  • ai_platform_engineering/llm_wrapper/: the single build choke point and the openai-compatible provider.
  • POST /api/mcp-servers/agent-context and the caller-bound agent context in #2909: minting short-lived signed identity.
  • ai_platform_engineering/mcp/litellm/: LiteLLM admin as MCP tools; a reference for the LiteLLM adapter.
  • 2026-09-24-remove-cnoe-agent-utils: provides the openai-compatible provider this spec builds on.