Agentgateway generic OpenRouter passthrough lane
Agentgateway generic OpenRouter passthrough lane
Section titled “Agentgateway generic OpenRouter passthrough lane”Status: Design (pre-plan) Bead: hl-rpm9 Date: 2026-06-20 Deciders: Sean Brandt
Overview
Section titled “Overview”Add a dedicated, transparent OpenRouter passthrough host to agentgateway
(openrouter-gw.fzymgc.house) so in-cluster clients can call any OpenRouter
OpenAI-compatible endpoint with any model slug and any request parameters,
without per-model alias registration. The lane keeps OpenTelemetry
usage/token labels (via the Detect route type) and offers a client-selectable
Zero-Data-Retention (ZDR) variant for callers that need the privacy guarantee.
This complements — does not replace — the existing curated llm-gw.fzymgc.house
lane (ADR hl-hv3, hl-6nm), which stays the blessed, alias-gated path for the
cluster’s standard workloads (octopus, engram, reviews).
Motivation
Section titled “Motivation”The curated openrouter backend on llm-gw.fzymgc.house already forwards the
request’s model field unchanged and already passes arbitrary params through
(it carries no ai.overrides). What it lacks is endpoint coverage: the
llm-chat HTTPRoute only matches /v1/chat/completions and /v1/models, and
llm-embeddings only /v1/embeddings. There is no path for /v1/responses,
/v1/messages (Anthropic-style), /v1/messages/count_tokens, or future
OpenRouter endpoints, and no single lane intended for “send whatever OpenRouter
accepts.”
The goal is a lane where a client configured exactly as if it were talking to
https://openrouter.ai/api/v1 works unchanged — only the base host swaps to the
in-cluster gateway, gaining the cluster virtual-key gate and OTel pipeline.
Decisions
Section titled “Decisions”-
Dedicated host
openrouter-gw.fzymgc.house, not a catch-all on the curatedllm-gwhost. Clean isolation: no Gateway-API longest-prefix interplay with the curated routes, and a clear mental model (“this host is raw OpenRouter”). Cost: one cert SAN + one Gateway listener + one HostMapping alias (all in-repo; see Components). -
Detectroute type for the non-ZDR lane (routes: { "*": Detect }), notPassthrough.Detectforwards the body as-is but still extracts themodelfield and usage for telemetry/rate-limiting, preserving the cluster’s OTel provider/model/token labels.Passthroughwould go dark on telemetry for no added benefit at this layer. -
Transparent 1:1 path mirroring. Under
Detect/Passthrough, agentgateway ignorespathPrefixand forwards the client’s path verbatim (set_default_pathearly-returns). Therefore the contract is: clients set their base URL tohttps://openrouter-gw.fzymgc.house/api/v1(mirroring OpenRouter’s real/api/v1base). A clientPOST /api/v1/responsesis forwarded toopenrouter.ai/api/v1/responses. The generic backend omitspathPrefix(it would be ignored anyway). -
ZDR is a typed-route sibling backend — it cannot be a
*: Detectcatch-all.ai.overridesinjectszdr: trueinto the parsed request body; a forward-as-is lane has no parsed body to inject into. The ZDR backend therefore enumerates typed RouteTypes sopathPrefix + suffixreconstruction and the ZDR override both apply. This mirrors the existingopenrouter-zdrisolation accepted in ADR hl-0eb (ZDR is backend-global; no per-model selector).Typed set =
Completions+Embeddings— OpenRouter’s OpenAI-compatible surface (/api/v1/chat/completions,/api/v1/embeddings).MessagesandAnthropicTokenCountare deliberately excluded: those are Anthropic’s native/v1/messagessurface, which OpenRouter’s OpenAI-compatible API does not expose; routing them through an OpenAI provider would silently translate to/chat/completions.Responsesis a verify-and-add item (only include it once OpenRouter is confirmed to serve/api/v1/responses).No double-prefix. For typed RouteTypes the upstream suffix is the provider’s fixed canonical string per type —
openai::path_suffix:Completions → /chat/completions,Embeddings → /embeddings— appended topathPrefix: /api/v1, giving/api/v1/chat/completionsand/api/v1/embeddings. Theroutesmap key is used only for ends-with detection of the RouteType, not as the upstream suffix — so the client’s/api/v1/chat/completionsmatches the/v1/chat/completionskey (ends-with) yet reconstructs to a single/api/v1/..., never/api/v1/v1/.... Both lanes therefore work from the same client base URL (.../api/v1). -
Lane selection uses two separate HTTPRoutes, not two rules in one route, header-selected by the
x-agentgateway-backend: openrouter-generic-zdrrequest header. Both routes matchPathPrefix "/"on the same host; the ZDR route adds the header match and is chosen by Gateway API’s header-count precedence tiebreaker (more matching headers wins on a path tie), not by rule list-order. This is the exact, proven in-cluster pattern ofllm-chat/llm-chat-zdr(two HTTPRoutes sharing a path; the ZDR one adds the header) — a single HTTPRoute with two identical-path rules would be ambiguous/unreachable under naive first-match, so it is avoided. A request without the header matches only the generic route; a request with it matches both and the header tiebreaker selects ZDR.
Architecture
Section titled “Architecture”openrouter-gw.fzymgc.house (NEW Gateway listener; SAN on agentgateway-tls; HostMapping alias -> 192.168.20.155) client base_url: https://openrouter-gw.fzymgc.house/api/v1 │ │ vkey gate (AgentgatewayPolicy apiKeyAuthentication, Strict; │ targets BOTH routes below) ▼ Two HTTPRoutes on the same host, same path "/", header-count tiebreaker:
HTTPRoute: openrouter-generic (PathPrefix "/", no header) └─► openrouter-generic routes: { "*": Detect } pathPrefix ignored → forwards /api/v1/* verbatim → openrouter.ai (transparent proxy; usage telemetry scraped)
HTTPRoute: openrouter-generic-zdr (PathPrefix "/" + header x-agentgateway-backend: openrouter-generic-zdr) └─► openrouter-generic-zdr routes: { /v1/chat/completions: Completions, /v1/embeddings: Embeddings } pathPrefix /api/v1 + fixed suffix; ai.overrides zdr=trueA request without the ZDR header matches only openrouter-generic. A request
with the header matches both routes (both PathPrefix "/"); the ZDR route’s
extra header match wins the Gateway API precedence tiebreaker.
Both backends target host: openrouter.ai:443, authenticate with the existing
agentgateway-openrouter secret, and initiate TLS (tls: {}).
Components
Section titled “Components”New file: argocd/app-configs/agentgateway/openrouter-generic.yaml
(registered in kustomization.yaml). Holds both backends, the HTTPRoute, and the
vkey policy for the new route.
-
AgentgatewayBackend/openrouter-genericspec:ai:provider:openai: {} # no model pin → model from requesthost: openrouter.aiport: 443# pathPrefix omitted: Detect ignores it; client sends /api/v1/* verbatimpolicies:auth: { secretRef: { name: agentgateway-openrouter } }tls: {}ai:routes:"*": Detect # forward as-is + scrape usage telemetry -
AgentgatewayBackend/openrouter-generic-zdrspec:ai:provider:openai: {}host: openrouter.aiport: 443pathPrefix: /api/v1 # typed RouteTypes apply prefix + suffixpolicies:auth: { secretRef: { name: agentgateway-openrouter } }tls: {}ai:routes:/v1/chat/completions: Completions/v1/embeddings: Embeddings# /v1/responses: Responses # add once OpenRouter /api/v1/responses confirmedoverrides:- field: providervalue: { zdr: true, require_parameters: true } -
HTTPRoute/openrouter-generic— parentRef GatewayagentgatewaysectionName: openrouter-gw, hostnameopenrouter-gw.fzymgc.house, one rule:match: PathPrefix "/"(no header filter) → backendopenrouter-generic. -
HTTPRoute/openrouter-generic-zdr— same parentRef/host, one rule:match: PathPrefix "/"plus headerx-agentgateway-backend: openrouter-generic-zdr→ backendopenrouter-generic-zdr. Separate route (not a second rule) so Gateway API’s header-count precedence selects it on the path tie — the provenllm-chat-zdrpattern. -
AgentgatewayPolicy/openrouter-generic-apikey—apiKeyAuthenticationmodeStrict,secretRef: agentgateway-vkeys,targetRefs: [HTTPRoute openrouter-generic, HTTPRoute openrouter-generic-zdr](both routes, so the vkey gate cannot be bypassed via the ZDR header). A dedicated policy keeps the new lane self-contained and independently revertible, rather than extending the sharedllm-apikeytargetRefs.
Edited files:
gateway.yaml— add listeneropenrouter-gw(HTTPS:443, hostnameopenrouter-gw.fzymgc.house, TLS Terminateagentgateway-tls,allowedRoutes: { namespaces: { from: Same } }).certificate.yaml— addopenrouter-gw.fzymgc.housetodnsNames(cert-manager re-issues via the existing Cloudflare DNS-01 issuer). Note: this name becomes visible in public CT logs, consistent with the existingllm-gw/mcp-gwSAN trade-off.hostmapping.yaml— addopenrouter-gw.fzymgc.housetoaliasesand a tag. This is the actual DNS mechanism; the Gateway’srouter-hostsannotation is a no-op (the operator watches Ingress/Service, not Gateway).kustomization.yaml— addopenrouter-generic.yamltoresources.
Reused without change:
agentgateway-openrouterauth secret,agentgateway-vkeysvirtual keys,agentgateway-tlscert secret.llm-max-bufferpolicy — already Gateway-scoped (targetRefs: Gateway agentgateway), so the new listener inherits the 32Mi buffer automatically.
Data flow
Section titled “Data flow”- Client (base URL
https://openrouter-gw.fzymgc.house/api/v1, headerAuthorization: Bearer <virtual-key>) sends e.g.POST /api/v1/responseswith{ "model": "anthropic/claude-opus-4.7", ... }. - Gateway terminates TLS, the vkey policy enforces a valid virtual key (Strict).
- HTTPRoute
openrouter-genericrule 1 (no special header) → backendopenrouter-generic. Detectreadsmodelfor telemetry, leaves body/path untouched, injects theagentgateway-openrouterupstreamAuthorization, initiates TLS, forwards toopenrouter.ai/api/v1/responses.- Response streams back; OTel records provider=openrouter, model, token usage.
ZDR path: client adds header x-agentgateway-backend: openrouter-generic-zdr;
the request matches both routes on the / path, and the ZDR route wins on the
header-count tiebreaker → backend openrouter-generic-zdr. The typed RouteType
parses the body, ai.overrides sets provider.zdr=true +
require_parameters=true, the upstream path reconstructs as pathPrefix +
fixed canonical suffix (e.g. /api/v1/chat/completions), and it forwards.
Non-goals (YAGNI)
Section titled “Non-goals (YAGNI)”- No gateway-side retry on the generic lane (interactive clients retry themselves; the embeddings retry policy exists only for octopus batch indexing).
- No model alias map — native slugs only; aliasing is the curated lane’s job.
- No new virtual keys / Vault paths — reuse
agentgateway-vkeys. If a separate quota/identity for this lane is wanted later, it is an additive follow-up. - No
Realtime/websocket support in v1 (would requirePassthroughand loses telemetry; revisit if a concrete need appears). - The curated
llm-gwlane and its routes/aliases are untouched.
Testing / verification plan
Section titled “Testing / verification plan”Pre-merge (static):
kubectl --context fzymgc-house apply --dry-run=server -k argocd/app-configs/agentgateway— validates the new backends/routes/policy and theroutesenum values against the deployed v1.3.0-alpha.1 CRD.- Confirm cert-manager re-issues
agentgateway-tlswith the new SAN before the listener serves traffic. - Confirm the Gateway-scoped
llm-max-bufferpolicy is inherited by the newopenrouter-gwlistener (nosectionNameon its targetRef → all listeners).
Post-deploy (live, gated):
- DNS resolves
openrouter-gw.fzymgc.house→ 192.168.20.155 (HostMapping). - Non-ZDR:
curl https://openrouter-gw.fzymgc.house/api/v1/chat/completionswith a native slug + virtual key → 200 from OpenRouter (transparent path). GET /api/v1/modelsreturns OpenRouter’s full catalog (a passthrough bonus).- Missing/invalid virtual key on both routes → 401/403 (the ZDR header must not bypass the vkey gate).
- Route selection (addresses review #1): a request with the
x-agentgateway-backend: openrouter-generic-zdrheader lands on the ZDR backend, and one without it lands on the generic backend — confirm via OTel/backend labels that the header-count tiebreaker actually selects the ZDR route on the shared/path. This is the riskiest assumption; verify before relying on ZDR. - ZDR path reconstruction (addresses review #2): confirm a ZDR
/api/v1/chat/completionsrequest reaches OpenRouter at/api/v1/chat/completions(single prefix, not/api/v1/v1/...) and returns 200 — a double-prefix would surface as a 404. - ZDR enforcement: response/telemetry confirms
provider.zdrwas applied; a ZDR request for a model that requires params errors closed rather than serving without ZDR. - ClickStack: confirm
Detectemits provider/model/token labels for the generic lane (the reasonDetectwas chosen overPassthrough), including for streamed (stream: true) responses.
Open risks
Section titled “Open risks”Detecttelemetry completeness for streamed (SSE) responses and for non-chat endpoints (/v1/responses) — verify token usage is captured, not just request metadata.- Streaming under
Detect— confirmstream: trueresponses pass through cleanly (expected, since the body is forwarded as-is). - Header-count precedence on a shared
/path — the ZDR route selection relies on Gateway API’s header tiebreaker between two same-path routes. Proven in-cluster forllm-chat/llm-chat-zdr(which share a specific path); this design shares the/catch-all path. Behaviorally identical under the spec, but must be live-verified (see verification plan) before ZDR is trusted. - ZDR-on-typed coverage — only the enumerated endpoints (
Completions,Embeddings) get the override. An un-enumerated path sent with the ZDR header falls to the defaultCompletionstype and is forwarded (parsed as chat), not rejected — so a client mis-using the ZDR header on a non-chat, non-embedding endpoint could get a malformed-but-forwarded request rather than a clean error. Documented as a known bound; clients should send the ZDR header only on chat/embeddings traffic.
References
Section titled “References”- ADR hl-hv3 — agentgateway as unified LLM/MCP data plane (replaces LiteLLM).
- ADR hl-6nm — split model routing (OpenRouter reviews + in-cluster embeddings).
- ADR hl-0eb — ZDR via
ai.overrides(backend-global; supersedes hl-env). - Deployed CRD: agentgateway v1.3.0-alpha.1
AgentgatewayBackend.routesenum =[AnthropicTokenCount, Completions, Detect, Embeddings, Messages, Models, Passthrough, Realtime, Responses]; keys match ends-with;*wildcard; defaultCompletions. (verified against the live CRD; bead hl-rpm9) - agentgateway upstream behavior (verified via deepwiki against
agentgateway/agentgateway, recorded in bead hl-rpm9):set_default_pathearly-returns forDetect/Passthrough→pathPrefixignored, client path forwarded verbatim (basis for the 1:1 proxy contract).- typed RouteTypes build the upstream path from
pathPrefix+ a fixedopenai::path_suffix(route_type)(Completions → /chat/completions,Embeddings → /embeddings) — therouteskey is only an ends-with detector, so no double-prefix. Detectstill reads themodelfield for telemetry.
- Existing patterns:
llm-routes.yaml(routes),llm-backends.yaml(backends),llm-policies.yaml(openrouter-zdrbackend +llm-chat-zdrseparate-route header selection — the proven header-count-tiebreaker precedent —llm-apikeyvkey gate,llm-max-bufferGateway-scoped buffer),hostmapping.yaml(DNS),certificate.yaml(SAN). - Bead hl-rpm9 — grounding traces (probe/deepwiki/CRD) and design-review rounds.