Skip to content

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/. Never kubectl apply to the cluster — ArgoCD owns sync. The only cluster interaction is --dry-run validation 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 bookmark feat/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/v1alpha1
kind: AgentgatewayBackend
metadata:
name: openrouter-generic
namespace: agentgateway
spec:
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/v1alpha1
kind: AgentgatewayBackend
metadata:
name: openrouter-generic-zdr
namespace: agentgateway
spec:
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/v1
kind: HTTPRoute
metadata:
name: openrouter-generic
namespace: agentgateway
spec:
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/v1
kind: HTTPRoute
metadata:
name: openrouter-generic-zdr
namespace: agentgateway
spec:
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/v1alpha1
kind: AgentgatewayPolicy
metadata:
name: openrouter-generic-apikey
namespace: agentgateway
spec:
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 like https://openrouter.ai/api/v1: set the client base URL to https://openrouter-gw.fzymgc.house/api/v1 and 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 header x-agentgateway-backend: openrouter-generic-zdr (chat/embeddings only; ZDR is enforced server-side via provider.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]"


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:

Terminal window
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 agentgateway app Synced/Healthy, and cert-manager has re-issued agentgateway-tls with 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:

Terminal window
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:

Terminal window
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.