fovea LLM role routing via agentgateway — 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: Route fovea’s scout and deepdive roles from Anthropic-direct to ZDR open models (DeepSeek V4-Flash + GLM-5.2) through the cluster agentgateway, with per-aspect model tiering, in shadow mode.
Architecture: Add a dedicated ZDR agentgateway backend for GLM-5.2 and two header-free path-prefix HTTPRoutes (reusing the existing openrouter-zdr Flash lane for scout); mint a vk_fovea virtual key; repoint fovea’s two roles at the gateway via provider: openai + per-role base_url, routing the correctness/security scout aspects to the GLM-5.2 (deepdive) role.
Tech Stack: ArgoCD (multi-source Application + app-of-apps), agentgateway (agentgateway.dev/v1alpha1 + Gateway API HTTPRoute), External Secrets Operator, HashiCorp Vault, OpenRouter (ZDR endpoints), Helm (OCI chart ghcr.io/seanb4t/charts/fovea 0.4.3).
Model labels (for plan-to-beads): Tasks 1–5 and 7 are mechanical YAML / secret edits → haiku. Task 6 (config cutover) and Task 8 (live verification + judgment) → sonnet.
Spec: docs/engineering/specs/2026-06-25-fovea-agentgateway-model-routing-design.md
Design bead: hl-3w5i
Merge sequencing (safety): Land Tasks 1–5 + 7 first (inert plumbing — the gateway lane and openai_api_key secret exist but nothing uses them yet) as PR-A; verify the fovea-app Secret gained openai_api_key; then Task 6 (the cutover) as PR-B. This avoids a transient CrashLoopBackOff if the Deployment rolls before ESO populates the new key.
Task 1: Mint vk_fovea and store it in Vault (manual operator step)
Section titled “Task 1: Mint vk_fovea and store it in Vault (manual operator step)”The virtual key is an operator-chosen bearer token stored in two Vault paths (per the karakeep/mealie pattern): the gateway accept-list path and fovea’s own client path — same value in both. Secrets never live in git.
Files: none (Vault only).
- Step 1: Generate a random virtual key
Run:
VK_FOVEA="vk_fovea_$(openssl rand -hex 24)"echo "$VK_FOVEA" # capture this; used in both Vault writes below- Step 2: Write the accept-list copy (gateway path)
Run (operator, authenticated to Vault):
vault kv patch fzymgc-house/cluster/agentgateway vk_fovea="$VK_FOVEA"- Step 3: Write the client copy (fovea path)
Run:
vault kv patch fzymgc-house/cluster/fovea openai_api_key="$VK_FOVEA"- Step 4: Verify both properties exist
Run:
vault kv get -field=vk_fovea fzymgc-house/cluster/agentgateway >/dev/null && echo "agentgateway#vk_fovea OK"vault kv get -field=openai_api_key fzymgc-house/cluster/fovea >/dev/null && echo "fovea#openai_api_key OK"Expected: both OK lines print (non-zero exit if a property is missing).
- Step 5: Commit
No repo change in this task. Record completion in the bead note instead of a commit.
Task 2: Add vk_fovea to the agentgateway vkey accept-list
Section titled “Task 2: Add vk_fovea to the agentgateway vkey accept-list”Files:
-
Modify:
argocd/app-configs/agentgateway/secrets.yaml(theagentgateway-vkeysExternalSecret) -
Step 1: Add the template.data entry
In the agentgateway-vkeys ExternalSecret spec.target.template.data map, add the
fovea client id after the mealie line:
# mealie recipe parsing + photo import via the LLM gateway (hl-a4a2). mealie: '{"key":"{{ .vk_mealie }}","metadata":{"group":"service"}}' # fovea PR-review engine — scout (Flash) + deepdive (GLM-5.2) via the # ZDR chat lanes (hl-3w5i). fovea: '{"key":"{{ .vk_fovea }}","metadata":{"group":"service"}}'- Step 2: Add the data entry
In the same ExternalSecret spec.data list, add (after the vk_mealie block):
- secretKey: vk_fovea remoteRef: key: fzymgc-house/cluster/agentgateway property: vk_fovea- Step 3: Validate YAML + kustomize render
Run:
yamllint -c .yamllint.yaml argocd/app-configs/agentgateway/secrets.yamlkubectl kustomize argocd/app-configs/agentgateway >/dev/null && echo "kustomize OK"Expected: yamllint clean; kustomize OK.
- Step 4: Commit
Commit using VCS-appropriate commands per references/vcs-preamble.md. Message:
feat(agentgateway): add vk_fovea to the LLM vkey accept-list [hl-3w5i]
Task 3: Add the openrouter-zdr-glm ZDR backend
Section titled “Task 3: Add the openrouter-zdr-glm ZDR backend”Mirrors the existing openrouter-zdr backend (Flash), but pins GLM-5.2. ZDR
overrides are backend-global, so GLM needs its own backend.
Files:
-
Modify:
argocd/app-configs/agentgateway/llm-policies.yaml(add a newAgentgatewayBackendbesideopenrouter-zdr) -
Step 1: Add the backend
Insert a new document after the existing openrouter-zdr AgentgatewayBackend:
---# Dedicated ZDR backend for fovea's deepdive role (+ strong scout aspects):# GLM-5.2. ZDR overrides (backend.ai.overrides) are backend-global, so GLM lives# in its own backend, parallel to openrouter-zdr (deepseek-v4-flash). [hl-3w5i]apiVersion: agentgateway.dev/v1alpha1kind: AgentgatewayBackendmetadata: name: openrouter-zdr-glm namespace: agentgatewayspec: ai: provider: openai: model: z-ai/glm-5.2 host: openrouter.ai port: 443 pathPrefix: /api/v1 policies: auth: secretRef: name: agentgateway-openrouter tls: {} ai: modelAliases: or-glm-5-2-zdr: z-ai/glm-5.2 overrides: - field: provider value: zdr: true require_parameters: true- Step 2: Validate YAML + kustomize render
Run:
yamllint -c .yamllint.yaml argocd/app-configs/agentgateway/llm-policies.yamlkubectl kustomize argocd/app-configs/agentgateway >/dev/null && echo "kustomize OK"Expected: yamllint clean; kustomize OK.
- Step 3: Commit
Message: feat(agentgateway): add openrouter-zdr-glm ZDR backend (GLM-5.2) [hl-3w5i]
Task 4: Add the two fovea HTTPRoutes + extend the vkey-auth policy
Section titled “Task 4: Add the two fovea HTTPRoutes + extend the vkey-auth policy”Mirrors the proven llm-embeddings-engram custom-prefix route (no rewrite filter;
the backend’s pathPrefix: /api/v1 does the upstream rewrite).
Files:
- Modify:
argocd/app-configs/agentgateway/llm-routes.yaml(two newHTTPRoutes) - Modify:
argocd/app-configs/agentgateway/llm-policies.yaml(thellm-apikeypolicytargetRefs) - Step 1: Add the two HTTPRoutes
Append to llm-routes.yaml:
---# fovea scout role → Flash via the existing openrouter-zdr backend. Custom prefix# /fovea-scout/v1 (header-free) mirrors llm-embeddings-engram; longest-prefix# distinct from /v1/chat/completions. [hl-3w5i]apiVersion: gateway.networking.k8s.io/v1kind: HTTPRoutemetadata: name: llm-fovea-scout namespace: agentgatewayspec: parentRefs: - { group: gateway.networking.k8s.io, kind: Gateway, name: agentgateway, sectionName: llm-gw } hostnames: ["llm-gw.fzymgc.house"] rules: - matches: - { path: { type: PathPrefix, value: /fovea-scout/v1/chat/completions } } backendRefs: - { group: agentgateway.dev, kind: AgentgatewayBackend, name: openrouter-zdr, weight: 1 }---# fovea deepdive role (+ correctness/security scout aspects) → GLM-5.2 ZDR. [hl-3w5i]apiVersion: gateway.networking.k8s.io/v1kind: HTTPRoutemetadata: name: llm-fovea-deepdive namespace: agentgatewayspec: parentRefs: - { group: gateway.networking.k8s.io, kind: Gateway, name: agentgateway, sectionName: llm-gw } hostnames: ["llm-gw.fzymgc.house"] rules: - matches: - { path: { type: PathPrefix, value: /fovea-deepdive/v1/chat/completions } } backendRefs: - { group: agentgateway.dev, kind: AgentgatewayBackend, name: openrouter-zdr-glm, weight: 1 }- Step 2: Extend the
llm-apikeypolicy targetRefs
In llm-policies.yaml, the llm-apikey AgentgatewayPolicy spec.targetRefs list
currently ends with llm-chat-zdr. Add the two fovea routes:
- { group: gateway.networking.k8s.io, kind: HTTPRoute, name: llm-chat-zdr } - { group: gateway.networking.k8s.io, kind: HTTPRoute, name: llm-fovea-scout } - { group: gateway.networking.k8s.io, kind: HTTPRoute, name: llm-fovea-deepdive }- Step 3: Validate YAML + kustomize render
Run:
yamllint -c .yamllint.yaml argocd/app-configs/agentgateway/llm-routes.yaml argocd/app-configs/agentgateway/llm-policies.yamlkubectl kustomize argocd/app-configs/agentgateway >/dev/null && echo "kustomize OK"Expected: yamllint clean; kustomize OK.
- Step 4: Commit
Message: feat(agentgateway): fovea ZDR chat routes + vkey gate [hl-3w5i]
Task 5: Add openai_api_key to the fovea ExternalSecret
Section titled “Task 5: Add openai_api_key to the fovea ExternalSecret”Files:
-
Modify:
argocd/app-configs/fovea/external-secret.yaml -
Step 1: Add the template.data line
In the fovea-app ExternalSecret spec.target.template.data, add after the
anthropic_api_key line:
anthropic_api_key: "{{ .anthropic_api_key }}" openai_api_key: "{{ .openai_api_key }}"- Step 2: Add the data entry
In spec.data, add after the anthropic_api_key remoteRef:
- secretKey: openai_api_key remoteRef: { key: fzymgc-house/cluster/fovea, property: openai_api_key }Also update the file’s top comment from “Only the 3 keys” to “4 keys” (the chart
now also consumes openaiKey).
- Step 3: Validate YAML + kustomize render
Run:
yamllint -c .yamllint.yaml argocd/app-configs/fovea/external-secret.yamlkubectl kustomize argocd/app-configs/fovea >/dev/null && echo "kustomize OK"Expected: yamllint clean; kustomize OK.
- Step 4: Commit
Message: feat(fovea): add openai_api_key (vk_fovea) to ExternalSecret [hl-3w5i]
Task 6: Cut fovea over to the gateway (roles + aspect tiering + openaiKey)
Section titled “Task 6: Cut fovea over to the gateway (roles + aspect tiering + openaiKey)”This is the activating change — land as PR-B after PR-A is synced and the
fovea-app Secret shows openai_api_key.
Files:
-
Modify:
argocd/cluster-app/templates/fovea.yaml -
Step 1: Replace the
gatewayconfig block
Replace the current gateway block — its explanatory comment plus the
provider: anthropic roles from PR-1380 (~lines 35–47) — with the gateway-routed,
tiered version:
# 0.4.x per-role gateway model. Both roles route through the cluster # agentgateway (provider: openai) to ZDR OpenRouter lanes via header-free # path-prefix routes. scout=DeepSeek V4-Flash (cheap), deepdive=GLM-5.2. # Per-aspect tiering: correctness/security scout aspects use the deepdive # role (GLM-5.2) — the override value is an llm.Role (closed set: # scout|deepdive), not a model id. types/tests inherit the scout role. # gate is a confidence threshold, not an LLM role. [hl-3w5i] gateway: openai: # global default = scout lane; both roles override explicitly below. base_url: "https://llm-gw.fzymgc.house/fovea-scout/v1" roles: scout: provider: openai model: or-deepseek-v4-flash-zdr # backend pins the model base_url: "https://llm-gw.fzymgc.house/fovea-scout/v1" deepdive: provider: openai model: or-glm-5-2-zdr base_url: "https://llm-gw.fzymgc.house/fovea-deepdive/v1" scout: aspects: correctness: { model: deepdive } # → GLM-5.2 (strong tier) security: { model: deepdive } # → GLM-5.2 (strong tier) # types, tests inherit the scout role (Flash) — no override- Step 2: Add
openaiKey, retainanthropicKey
After the anthropicKey line in the valuesObject (currently line ~65), add:
anthropicKey: { name: fovea-app, key: anthropic_api_key } # retained but unused while both roles are provider: openai — keeps the # break-glass flip back to Anthropic-direct a one-line change. [hl-3w5i] openaiKey: { name: fovea-app, key: openai_api_key }- Step 3: Validate — lint, app-of-apps render, child-chart render
Run:
yamllint -c .yamllint.yaml argocd/cluster-app/templates/fovea.yamlhelm template cluster-app argocd/cluster-app >/dev/null && echo "cluster-app OK"# Confirm the child chart renders the tiered gateway config + openai key wiring:WORK="$(mktemp -d)"helm template cluster-app argocd/cluster-app 2>/dev/null \ | yq 'select(.kind=="Application" and .metadata.name=="fovea") | .spec.sources[0].helm.valuesObject' > "$WORK/v.yaml"helm template fovea oci://ghcr.io/seanb4t/charts/fovea --version 0.4.3 -f "$WORK/v.yaml" 2>/dev/null \ | yq 'select(.kind=="ConfigMap" and .data."config.yaml") | .data."config.yaml"' \ | yq '{"roles": .gateway.roles, "aspects": .scout.aspects}'Expected: yamllint clean; cluster-app OK; the final block shows
scout.provider: openai / model: or-deepseek-v4-flash-zdr,
deepdive.provider: openai / model: or-glm-5-2-zdr, and
aspects.correctness.model: deepdive, aspects.security.model: deepdive.
- Step 4: Commit
Message: feat(fovea): route scout/deepdive through agentgateway to ZDR open models [hl-3w5i]
Task 7: Document the routing + new secret
Section titled “Task 7: Document the routing + new secret”Files:
- Modify:
docs/reference/services.md - Modify:
docs/reference/secrets.md - Step 1: Update services.md
In the fovea / agentgateway client section, document that fovea routes LLM calls
through llm-gw.fzymgc.house over ZDR lanes: scout → /fovea-scout/v1
(DeepSeek V4-Flash, openrouter-zdr), deepdive → /fovea-deepdive/v1
(GLM-5.2, openrouter-zdr-glm); correctness/security scout aspects use the
deepdive (GLM-5.2) role; auth via vk_fovea.
- Step 2: Update secrets.md
Add vk_fovea to the agentgateway vkey list (Vault fzymgc-house/cluster/agentgateway#vk_fovea)
and note the fovea client copy at fzymgc-house/cluster/fovea#openai_api_key.
- Step 3: Validate
Run:
rumdl check docs/reference/services.md docs/reference/secrets.mdExpected: no issues.
- Step 4: Commit
Message: docs(reference): document fovea→agentgateway ZDR routing + vk_fovea [hl-3w5i]
Task 8: Merge, sync, and verify in shadow
Section titled “Task 8: Merge, sync, and verify in shadow”Files: none (verification only). Run after PR-B syncs.
- Step 1: Confirm ArgoCD synced both apps
Use the Kubernetes MCP (read-only) to confirm the agentgateway and fovea
Applications are Synced/Healthy and the fovea Deployment image is still
ghcr.io/seanb4t/fovea:0.4.3.
- Step 2: Confirm the secret + pod
Confirm the fovea-app Secret now has an openai_api_key key, and the fovea
pod is 1/1 Running with 0 restarts (no boot config-validation error).
- Step 3: Confirm no gateway auth failure
Check fovea pod logs (or ClickStack) for 401/auth errors talking to
llm-gw.fzymgc.house. Expected: none (vk_fovea accepted).
- Step 4: Drive a test PR and confirm tiering + ZDR + verified funnel
On an App-installed repo, open a test PR. In ClickStack, confirm: scout fan-out
runs across both tiers (requests to openrouter-zdr for types/tests, and
openrouter-zdr-glm for correctness/security); deepdive reaches the
verified funnel stage on GLM-5.2 with no require_parameters/structured-output
failure; and requests route to the ZDR backends (cross-check the OpenRouter
activity dashboard — ZDR endpoint, params honored). publish_enabled stays
false, so no PR comment is posted.
- Step 5: Record results on the bead
Append a bd note hl-3w5i with the verification outcome. Go-live (flipping
publish_enabled: true) remains the separate hl-uh07.9 gate, out of scope here.
Out of scope
Section titled “Out of scope”Go-live flip (hl-uh07.9); the sonnet ZDR fallback backend (documented in the
spec, deferred); mesh egress (hl-z5fc); retrieval/embedding routing.