Agentgateway generic OpenRouter passthrough lane — Implementation Plan
Agentgateway generic OpenRouter passthrough lane — Implementation Plan
Section titled “Agentgateway generic OpenRouter passthrough lane — Implementation Plan”For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add a dedicated transparent OpenRouter passthrough host (openrouter-gw.fzymgc.house) to agentgateway so clients can call any OpenRouter OpenAI-compatible endpoint with any model slug and any params, virtual-key-gated and OTel-observed, with an opt-in ZDR variant.
Architecture: A new HTTPS Gateway listener on a dedicated hostname routes all traffic to a Detect AgentgatewayBackend that forwards the client’s path verbatim to openrouter.ai (transparent 1:1 proxy; clients use base …/api/v1). A sibling typed-route backend, selected per-request by the x-agentgateway-backend: openrouter-generic-zdr header on a separate HTTPRoute (Gateway API header-count tiebreaker), injects provider.zdr=true via ai.overrides. A dedicated apiKeyAuthentication policy gates both routes.
Tech Stack: Kubernetes (kustomize), ArgoCD (GitOps), agentgateway CRDs v1alpha1 (deployed v1.3.0-alpha.1), Gateway API v1, cert-manager (Cloudflare DNS-01), router-hosts HostMapping CRD, ExternalSecrets (Vault).
Spec: docs/engineering/specs/2026-06-20-agentgateway-openrouter-generic-route-design.md
Design bead: hl-rpm9
Conventions for this plan:
- All edits are GitOps manifests under
argocd/app-configs/agentgateway/. Neverkubectl applyto the cluster — ArgoCD owns sync. The only cluster interaction is--dry-runvalidation and (post-merge) read-only live checks. - Validation command used throughout:
kubectl --context fzymgc-house apply --dry-run=server -k argocd/app-configs/agentgateway(server dry-run validates CR fields against the live agentgateway CRDs without persisting). - “Commit” steps use jj (colocated repo):
jj commit -m "<conventional message> [hl-rpm9]". Work stays on bookmarkfeat/openrouter-generic-route.
Task 1: Add the dedicated Gateway listener, cert SAN, and DNS alias
Section titled “Task 1: Add the dedicated Gateway listener, cert SAN, and DNS alias”Files:
- Modify:
argocd/app-configs/agentgateway/gateway.yaml - Modify:
argocd/app-configs/agentgateway/certificate.yaml - Modify:
argocd/app-configs/agentgateway/hostmapping.yaml - Step 1: Add the listener to the Gateway
In gateway.yaml, append a fourth listener after the ui listener (keep the existing three unchanged):
- name: openrouter-gw protocol: HTTPS port: 443 hostname: "openrouter-gw.fzymgc.house" tls: mode: Terminate certificateRefs: - { group: "", kind: Secret, name: agentgateway-tls } allowedRoutes: namespaces: { from: Same }- Step 2: Add the SAN to the certificate
In certificate.yaml, add the new name to spec.dnsNames (cert-manager re-issues via the existing cloudflare-acme-issuer):
dnsNames: - llm-gw.fzymgc.house - mcp-gw.fzymgc.house - agentgateway.fzymgc.house - openrouter-gw.fzymgc.house- Step 3: Add the DNS alias to the HostMapping
In hostmapping.yaml, add the new host to spec.aliases and a tag (this is the actual DNS mechanism; the Gateway annotation is a no-op). Keep spec.hostname: llm-gw.fzymgc.house unchanged — only aliases and tags gain an entry. The full spec block becomes:
spec: hostname: llm-gw.fzymgc.house aliases: - mcp-gw.fzymgc.house - openrouter-gw.fzymgc.house ip: "192.168.20.155" tags: - agentgateway - llm-gw - mcp-gw - openrouter-gw- Step 4: Validate the kustomize build
Run: kubectl --context fzymgc-house kustomize argocd/app-configs/agentgateway | rg -A2 "openrouter-gw.fzymgc.house"
Expected: the new listener hostname, cert dnsName, and HostMapping alias all render.
- Step 5: Commit
Run: jj commit -m "feat(agentgateway): add openrouter-gw listener, cert SAN, DNS alias [hl-rpm9]"
Task 2: Create the two OpenRouter passthrough backends
Section titled “Task 2: Create the two OpenRouter passthrough backends”Files:
- Create:
argocd/app-configs/agentgateway/openrouter-generic.yaml - Modify:
argocd/app-configs/agentgateway/kustomization.yaml - Step 1: Create the backends file
Create openrouter-generic.yaml with both backends. The non-ZDR backend uses Detect (forwards verbatim, keeps telemetry) and omits pathPrefix (Detect ignores it); the ZDR backend uses typed RouteTypes (so pathPrefix + fixed suffix reconstruction and the ZDR override both apply):
---# Generic transparent OpenRouter passthrough. Detect forwards the client path# verbatim (set_default_path early-returns -> pathPrefix ignored) while still# scraping usage/token telemetry. No model pin and no alias map: native slugs# and arbitrary params pass straight through. Clients set their base URL to# https://openrouter-gw.fzymgc.house/api/v1 (mirrors openrouter.ai/api/v1). [hl-rpm9]apiVersion: agentgateway.dev/v1alpha1kind: AgentgatewayBackendmetadata: name: openrouter-generic namespace: agentgatewayspec: ai: provider: openai: {} host: openrouter.ai port: 443 # pathPrefix intentionally omitted — Detect ignores it. policies: auth: secretRef: name: agentgateway-openrouter # reads "Authorization" key tls: {} ai: routes: "*": Detect---# Server-enforced-ZDR sibling. ai.overrides injects into the PARSED body, which# Detect/Passthrough cannot provide, so this lane uses typed RouteTypes. Typed# upstream path = pathPrefix(/api/v1) + fixed openai::path_suffix(type)# (Completions->/chat/completions, Embeddings->/embeddings); the routes-map key# is an ends-with detector only, so /api/v1/chat/completions matches without a# double prefix. Selected by the x-agentgateway-backend header (see Task 3).# [hl-rpm9; ADR hl-0eb]apiVersion: agentgateway.dev/v1alpha1kind: AgentgatewayBackendmetadata: name: openrouter-generic-zdr namespace: agentgatewayspec: ai: provider: openai: {} host: openrouter.ai port: 443 pathPrefix: /api/v1 policies: auth: secretRef: name: agentgateway-openrouter tls: {} ai: routes: /v1/chat/completions: Completions /v1/embeddings: Embeddings # /v1/responses: Responses # add once OpenRouter /api/v1/responses is confirmed (bd hl-rpm9.1) overrides: - field: provider value: zdr: true require_parameters: true- Step 2: Register the file in kustomize
In kustomization.yaml, add openrouter-generic.yaml to resources (after llm-policies.yaml):
- llm-policies.yaml - openrouter-generic.yaml- Step 3: Validate the backends against the live CRD
Run: kubectl --context fzymgc-house apply --dry-run=server -k argocd/app-configs/agentgateway
Expected: no errors; output includes
agentgatewaybackend.agentgateway.dev/openrouter-generic created (server dry run) and
.../openrouter-generic-zdr created (server dry run). A bad routes enum value would fail here.
- Step 4: Commit
Run: jj commit -m "feat(agentgateway): add openrouter-generic + openrouter-generic-zdr backends [hl-rpm9]"
Task 3: Add the two HTTPRoutes (generic + header-selected ZDR)
Section titled “Task 3: Add the two HTTPRoutes (generic + header-selected ZDR)”Files:
-
Modify:
argocd/app-configs/agentgateway/openrouter-generic.yaml -
Step 1: Append both HTTPRoutes
Append to openrouter-generic.yaml. Two separate routes sharing the / path; the ZDR route adds the header match and is selected by Gateway API’s header-count precedence tiebreaker — the proven llm-chat / llm-chat-zdr pattern:
---# Non-ZDR generic route: catch-all on the dedicated host -> openrouter-generic.apiVersion: gateway.networking.k8s.io/v1kind: HTTPRoutemetadata: name: openrouter-generic namespace: agentgatewayspec: parentRefs: - { group: gateway.networking.k8s.io, kind: Gateway, name: agentgateway, sectionName: openrouter-gw } hostnames: ["openrouter-gw.fzymgc.house"] rules: - matches: - { path: { type: PathPrefix, value: / } } backendRefs: - { group: agentgateway.dev, kind: AgentgatewayBackend, name: openrouter-generic, weight: 1 }---# ZDR route: same host + path, plus the x-agentgateway-backend header. A request# carrying the header matches BOTH routes; the extra header match wins the# Gateway API precedence tiebreaker -> openrouter-generic-zdr. Mirrors llm-chat-zdr.apiVersion: gateway.networking.k8s.io/v1kind: HTTPRoutemetadata: name: openrouter-generic-zdr namespace: agentgatewayspec: parentRefs: - { group: gateway.networking.k8s.io, kind: Gateway, name: agentgateway, sectionName: openrouter-gw } hostnames: ["openrouter-gw.fzymgc.house"] rules: - matches: - path: { type: PathPrefix, value: / } headers: - { name: x-agentgateway-backend, type: Exact, value: openrouter-generic-zdr } backendRefs: - { group: agentgateway.dev, kind: AgentgatewayBackend, name: openrouter-generic-zdr, weight: 1 }- Step 2: Validate
Run: kubectl --context fzymgc-house apply --dry-run=server -k argocd/app-configs/agentgateway
Expected: no errors; both httproute.gateway.networking.k8s.io/openrouter-generic and .../openrouter-generic-zdr report created (server dry run).
- Step 3: Commit
Run: jj commit -m "feat(agentgateway): add openrouter-generic HTTPRoutes (generic + ZDR header) [hl-rpm9]"
Task 4: Add the virtual-key auth policy for both routes
Section titled “Task 4: Add the virtual-key auth policy for both routes”Files:
-
Modify:
argocd/app-configs/agentgateway/openrouter-generic.yaml -
Step 1: Append the apiKeyAuthentication policy
Append to openrouter-generic.yaml. It targets both routes so the ZDR header cannot be used to bypass the virtual-key gate (mirrors llm-apikey):
---# Virtual-key gate on the generic lane. Targets BOTH routes so the ZDR header# path is equally gated. Reuses the existing agentgateway-vkeys secret — any# existing virtual key (e.g. vk_claude_code) works here. [hl-rpm9]apiVersion: agentgateway.dev/v1alpha1kind: AgentgatewayPolicymetadata: name: openrouter-generic-apikey namespace: agentgatewayspec: targetRefs: - { group: gateway.networking.k8s.io, kind: HTTPRoute, name: openrouter-generic } - { group: gateway.networking.k8s.io, kind: HTTPRoute, name: openrouter-generic-zdr } traffic: apiKeyAuthentication: mode: Strict secretRef: name: agentgateway-vkeys- Step 2: Validate the full app build
Run: kubectl --context fzymgc-house apply --dry-run=server -k argocd/app-configs/agentgateway
Expected: no errors across the whole app; agentgatewaypolicy.agentgateway.dev/openrouter-generic-apikey reports created (server dry run).
- Step 3: Confirm the Gateway-scoped buffer covers the new listener
Run: rg -A6 "name: llm-max-buffer" argocd/app-configs/agentgateway/llm-policies.yaml
Expected: its targetRefs is kind: Gateway, name: agentgateway with no sectionName — so the new openrouter-gw listener inherits the 32Mi buffer automatically. No change needed.
- Step 4: Commit
Run: jj commit -m "feat(agentgateway): vkey gate for openrouter-generic routes [hl-rpm9]"
Task 5: Document the new lane in the services catalog
Section titled “Task 5: Document the new lane in the services catalog”Files:
-
Modify:
docs/reference/services.md -
Step 1: Add the generic lane to the agentgateway docs
In docs/reference/services.md, follow the existing llm-gw.fzymgc.house entries (the catalog table near line 40, the “LLM URL” row near line 265, and the deployment-model paragraph near line 277). Add openrouter-gw.fzymgc.house and document the client contract precisely:
openrouter-gw.fzymgc.house— transparent OpenRouter passthrough. Use it exactly likehttps://openrouter.ai/api/v1: set the client base URL tohttps://openrouter-gw.fzymgc.house/api/v1and authenticate with a virtual key (agentgateway-vkeys). Any OpenRouter model slug and any params pass through; usage telemetry is captured (Detect). For Zero-Data-Retention, add request headerx-agentgateway-backend: openrouter-generic-zdr(chat/embeddings only; ZDR is enforced server-side viaprovider.zdr=true).
Also add openrouter-gw.fzymgc.house to the HostMapping note (it lists llm-gw/mcp-gw → Gateway LB).
- Step 2: Lint the docs
Run: rumdl check docs/reference/services.md
Expected: no new violations (fix any it reports in the lines you touched).
- Step 3: Commit
Run: jj commit -m "docs(services): document openrouter-gw generic passthrough lane [hl-rpm9]"
Task 6: Open the PR
Section titled “Task 6: Open the PR”Files: none (VCS only)
- Step 1: Push the bookmark
Run: jj bookmark set feat/openrouter-generic-route -r @ && jj git push -b feat/openrouter-generic-route
- Step 2: Open the PR
Run:
gh pr create --head feat/openrouter-generic-route \ --title "feat(agentgateway): generic OpenRouter passthrough lane (openrouter-gw) [hl-rpm9]" \ --body "Implements the spec at docs/engineering/specs/2026-06-20-agentgateway-openrouter-generic-route-design.md. Adds a dedicated transparent OpenRouter passthrough host with an opt-in ZDR variant. Design bead hl-rpm9.
🤖 Generated with [Claude Code](https://claude.com/claude-code)"Expected: PR URL printed. CI runs yamllint/rumdl/cocogitto + ArgoCD diff.
Task 7: Post-merge live verification (gated — run only after ArgoCD syncs)
Section titled “Task 7: Post-merge live verification (gated — run only after ArgoCD syncs)”Files: none (read-only live checks; do not kubectl apply)
Run these only after the PR merges and ArgoCD reports the
agentgatewayappSynced/Healthy, and cert-manager has re-issuedagentgateway-tlswith the new SAN. Use an existing virtual key for$VK.
- Step 1: DNS + cert
Run: dig +short openrouter-gw.fzymgc.house
Expected: 192.168.20.155.
Run: echo | openssl s_client -connect openrouter-gw.fzymgc.house:443 -servername openrouter-gw.fzymgc.house 2>/dev/null | openssl x509 -noout -text | rg -i "openrouter-gw"
Expected: the SAN is present.
- Step 2: Non-ZDR transparent chat (native slug)
Run:
curl -sS https://openrouter-gw.fzymgc.house/api/v1/chat/completions \ -H "Authorization: Bearer $VK" -H "Content-Type: application/json" \ -d '{"model":"anthropic/claude-haiku-4.5","messages":[{"role":"user","content":"ping"}],"max_tokens":8}' | jq '.choices[0].message.content, .model'Expected: a 200 completion echoing the model. Confirms verbatim path forwarding + native-slug passthrough.
- Step 3: Model catalog passthrough
Run: curl -sS https://openrouter-gw.fzymgc.house/api/v1/models -H "Authorization: Bearer $VK" | jq '.data | length'
Expected: a non-zero count (OpenRouter’s full catalog).
- Step 4: vkey gate holds on both routes
Run (no key): curl -s -o /dev/null -w "%{http_code}\n" https://openrouter-gw.fzymgc.house/api/v1/chat/completions -d '{}'
Run (no key + ZDR header): curl -s -o /dev/null -w "%{http_code}\n" https://openrouter-gw.fzymgc.house/api/v1/chat/completions -H "x-agentgateway-backend: openrouter-generic-zdr" -d '{}'
Expected: both 401 or 403 — the ZDR header must not bypass the gate.
- Step 5: ZDR route selection + path reconstruction (the riskiest assumptions)
Run:
curl -sS https://openrouter-gw.fzymgc.house/api/v1/chat/completions \ -H "Authorization: Bearer $VK" -H "x-agentgateway-backend: openrouter-generic-zdr" \ -H "Content-Type: application/json" \ -d '{"model":"deepseek/deepseek-v4-flash","messages":[{"role":"user","content":"ping"}],"max_tokens":8}' | jq '.model, .id'Expected: a 200 (not a 404). A 404 would mean the header tiebreaker failed to select the ZDR route or the typed path reconstructed as /api/v1/v1/... — investigate before trusting ZDR.
- Step 6: Telemetry + ZDR enforcement via ClickStack
Query default.otel_* for requests to the openrouter-generic backend and confirm provider/model/token labels are present (including for a stream:true request). Confirm via OTel/backend attribution that the Step 5 request hit openrouter-generic-zdr and that provider.zdr was applied. See docs/operations/clickstack.md.
- Step 7: Close the loop
Run: bd note hl-rpm9 "live-verify PASS: DNS/cert, non-ZDR chat, models, vkey gate (both routes), ZDR selection + path reconstruction, telemetry + ZDR enforcement."
If anything failed, file a bead with the symptom and the failing step instead.