Skip to content

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:

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

Terminal window
vault kv patch fzymgc-house/cluster/agentgateway vk_fovea="$VK_FOVEA"
  • Step 3: Write the client copy (fovea path)

Run:

Terminal window
vault kv patch fzymgc-house/cluster/fovea openai_api_key="$VK_FOVEA"
  • Step 4: Verify both properties exist

Run:

Terminal window
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 (the agentgateway-vkeys ExternalSecret)

  • 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:

Terminal window
yamllint -c .yamllint.yaml argocd/app-configs/agentgateway/secrets.yaml
kubectl 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 new AgentgatewayBackend beside openrouter-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/v1alpha1
kind: AgentgatewayBackend
metadata:
name: openrouter-zdr-glm
namespace: agentgateway
spec:
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:

Terminal window
yamllint -c .yamllint.yaml argocd/app-configs/agentgateway/llm-policies.yaml
kubectl 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 new HTTPRoutes)
  • Modify: argocd/app-configs/agentgateway/llm-policies.yaml (the llm-apikey policy targetRefs)
  • 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/v1
kind: HTTPRoute
metadata:
name: llm-fovea-scout
namespace: agentgateway
spec:
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/v1
kind: HTTPRoute
metadata:
name: llm-fovea-deepdive
namespace: agentgateway
spec:
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-apikey policy 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:

Terminal window
yamllint -c .yamllint.yaml argocd/app-configs/agentgateway/llm-routes.yaml argocd/app-configs/agentgateway/llm-policies.yaml
kubectl 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:

Terminal window
yamllint -c .yamllint.yaml argocd/app-configs/fovea/external-secret.yaml
kubectl 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 gateway config 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, retain anthropicKey

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:

Terminal window
yamllint -c .yamllint.yaml argocd/cluster-app/templates/fovea.yaml
helm 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]


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:

Terminal window
rumdl check docs/reference/services.md docs/reference/secrets.md

Expected: no issues.

  • Step 4: Commit

Message: docs(reference): document fovea→agentgateway ZDR routing + vk_fovea [hl-3w5i]


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.


Go-live flip (hl-uh07.9); the sonnet ZDR fallback backend (documented in the spec, deferred); mesh egress (hl-z5fc); retrieval/embedding routing.