Skip to content
Model routing and cost

Model routing and cost

Today, routing is static. Prompt caching drives the bill, and the model decides the outcome. An agent run gets one model from its data class (GLM-5.3 for public, Claude Opus 5.5 for internal), whatever its tier. The tier sets the run’s caps and its team, not its model. Nothing classifies a task yet: every task is triaged standard by the static fallback, and 10% go to frontier as a control group. On gcp-0, 95% of the input tokens agents sent in the last 24 hours were cache reads, which cut the bill about fourfold. On harder tasks the model changed the outcome more than any routing could: Claude was accepted on 13 of 15 issues, GLM-5.3 on none, in either harness.

This page owns the routing, tuning and cost angle. How the gateways authenticate and what they carry is on Gateways. What runs where is on Status, which wins over this page if the two disagree.

Figures dated 2026-10-18. Live numbers come from read-only queries against gcp-0’s VictoriaMetrics and its Task objects on 2026-10-18 (11:40 UTC). gcp-0 had run the factory for about 17 hours, so “24 h” and “7 d” windows cover the same traffic. Prices are the list prices the repository pins, read on 2026-10-16.

What is real today

PieceState on gcp-0, 2026-10-18Source
Agent model per data class (public → Z.ai, internal → Anthropic)Deployed, proven liveADR-0054, status Landed
tier-light, tier-standard, tier-frontier served by the agent gatewayDeployed. No factory tier asks for them: every tier names agent-defaultllm.yaml, anthropic.yaml, factory values
Triage classifier (complexity-classifier, C7)Design only. The URL is configured and nothing answers it, so every task falls back to standard with classifier: staticfactory values, live Task status
10% control group at frontier (triage.controlPercent)Deployed, live: 2 of 12 taskslive Task status
Run meter priced in USD, cache reads at their own rateDeployed, proven liveplan, internal run
Gateway budgets B1, B2, B6Deployed, in shadow: counted, never refusedrunbook 04, Part D
ai-gateway with tier-frontier (GLM-5.3)Deployed, CPU onlyaigatewayroute.yaml
Semantic Router MoM cascadeDeployed but idle: the router pod runs, and llm-platform is suspended, so no vLLM model exists to route tokubectl get ks llm-platform: no status, suspended
SR complexity signal, PII-gated frontier escalation, SR fail-openDesign only (SP4 PR 4: SR 0.3.0)SP4 design §2
LoRA canary on xplane-qwen-coderBuilt, serves only while llm-platform runsapps/base/ai/llm/qwen-coder.yaml
Endpoint picker (Gateway API Inference Extension)Built, enabled on no claimserving known gaps

Every routing decision, end to end

There are two callers and seven decisions. Only two of the seven look at the prompt: the Semantic Router on MoM, and the planned triage classifier (once per task, never per request).

    flowchart TB
  subgraph human["Humans and coding clients: ai-gateway (Envoy AI Gateway)"]
    C["Client: OpenCode, Continue, OpenWebUI, promptfoo"] -->|"model: name or MoM"| SR{"1 · Semantic Router<br/>only when model is MoM or auto"}
    SR -->|"rewrites body.model"| XM["x-ai-eg-model header"]
    C -->|"explicit name, no classification"| XM
    XM --> AR{"2 · AIGatewayRoute<br/>header match"}
    AR -->|"tier-frontier"| ZAI1["Z.ai glm-5.3"]
    AR -->|"xplane-qwen-coder"| CAN{"3 · LoRA canary<br/>90 / 10 weights"}
    AR -->|"other xplane-*"| V["vLLM Service"]
    CAN --> V
  end
  subgraph agent["Agent runs: agentgateway agent-router"]
    I["Issue labelled factory/ready"] --> T{"4 · Triage, once per task<br/>tier and template"}
    T --> R{"5 · Role<br/>reviewer takes the other tier"}
    R -->|"tiers.tier.model or reviewerModel"| N["Logical name<br/>agent-default, tier-*"]
    N --> D{"6 · Data class<br/>audience picks the listener"}
    D -->|"public :8080"| AL1{"7 · modelAliases"}
    D -->|"internal :8081"| AL2{"7 · modelAliases"}
    AL1 --> ZAI2["Z.ai glm-5.3, flashx, flash"]
    AL2 --> ANT["Anthropic opus-5-5, sonnet-5-5, haiku-5-5"]
  end
  
#DecisionDecided byWhenInputTuned in
1Which local model answers MoMSemantic Router, ext_proc at filter index 0Per requestThe prompt: domain classifier, keywords, lengthinfrastructure/base/vllm-semantic-router/helmrelease.yaml
2Which backend a model name reachesAIGatewayRoute rule on x-ai-eg-modelPer requestThe model name onlyinfrastructure/base/llm-gateway/aigatewayroute.yaml; claims with gateway.enabled
3Base model or LoRA adapterRoute weights rendered from gateway.canariesPer request, randomNoneapps/base/ai/llm/qwen-coder.yaml
4A task’s tier and templateFactory triageOnce, in ReceivedThe classifier’s tier (static fallback today), the class, controlPercenttooling/base/agent-factory/helm-values-configmap.yaml
5The model name a role asks forThe factoryPer runTier, role, templatesame file: tiers, templates.<name>.reviewerModel
6Which provider may see the dataThe run’s token audiencePer run, fixedspec.dataClassthe task (investigate forces internal)
7Which provider model a name meansmodelAliases on the backendPer request, staticThe nameinfrastructure/base/agent-gateway/llm.yaml, infrastructure/base/agent-gateway/anthropic.yaml

Decisions 4–7 are pinned by design: every name maps to one model at weight 100, with no fallback and no canary, so nothing re-routes a run mid-trajectory. Escalation is a new run at another tier, never a gateway failover (SP4 S7, S8).

Serving side: ai-gateway

The MoM cascade as configured

Semantic Router 0.2.0 (flux/sources/ocirepo-vllm-semantic-router.yaml) still uses the v0.2 schema. A ModernBERT domain classifier maps the prompt to an MMLU category, keyword and length rules add evidence, and the highest-priority decision that matches wins:

    flowchart TD
  P["model: MoM"] --> J{"prompt_guard<br/>jailbreak above 0.7"}
  J -->|"yes"| B["blocked"]
  J -->|"no"| CD{"domain code<br/>or code_keywords"}
  CD -->|"yes"| RK{"reasoning_keywords<br/>or prompt over 500 tokens"}
  RK -->|"yes · 110"| QCR["xplane-qwen-coder<br/>use_reasoning: true"]
  RK -->|"no · 100"| QC["xplane-qwen-coder"]
  CD -->|"no"| MP{"domain math or physics"}
  MP -->|"yes · 90"| Q8R["xplane-qwen3-8b<br/>thinking on"]
  MP -->|"no"| ML{"domain multilingual"}
  ML -->|"yes · 80"| Q8["xplane-qwen3-8b"]
  ML -->|"no · 50 or no match"| Q8D["xplane-qwen3-8b<br/>default_model"]
  
SignalConfiguredEffect on routing today
domain (MMLU categories)5 categories: math, physics, code, multilingual, otherPicks the model. multilingual maps to philosophy and law, a proxy rather than a language detector
keywordcode_keywords (16 terms: kubectl, yaml, debug…), reasoning_keywords (12: why, explain, compare…)Code keywords catch what the classifier misses. Reasoning keywords turn on chain-of-thought, the dearest knob in tokens
contextlong_query: 500 to 32,000 tokensTurns on reasoning for long code prompts
jailbreak (prompt_guard)threshold 0.7Blocks. A filter, not a route
piimodel loaded, threshold 0.7, every model allow_by_default: trueNone: no decision reads it
complexitynot configuredNone. Designed (SP4 S5) as a hard–easy margin of 0.12. The documented 0.70–0.75 never fires, because the margin stayed at or below 0.197 in tests (semantic-router#3246)
semantic_cacheoffWould answer from a similar past prompt. Off because tool-calling clients’ prompts all look alike, and a cached error re-poisons every retry

Two behaviours to know before you rely on MoM.

  • It sits in front of every request, and fails closed. The filter is on the http listener, not only on MoM, and has no failure_mode_allow, with a 60 s message_timeout (infrastructure/base/envoy-ai-gateway/envoypatchpolicy-semantic-router.yaml). An unreachable router fails every request on that listener, tier-frontier included (SP4 R13). A router that is up but not rewriting lets MoM reach vLLM, which answers 404 for an unknown model (the HelmRelease’s own note). The fix is SP4 PR 4, not built: two replicas, a PDB, and fail-open with a 2 s timeout.
  • Classification costs about 250–300 ms (gateways). A client that knows its model names it, and the router passes it through untouched. FIM traffic never uses MoM.

The designed cascade (SP4, not built)

The SP4 design replaces fleet names with roles that claims opt into (spec.gateway.aliases), and adds the one escalation that leaves the cluster:

BandRoleServed by
hard, no PIItier-frontierGLM through llm-gateway
hard with PII, medium, math or physicsmom-standard, thinking onxplane-qwen3-8b
codemom-codexplane-qwen-coder, canary still applied
easy, default, or router downmom-lightxplane-qwen3-8b, thinking off

Its verdict rule (SC-9) is set in advance: unless routed MoM reaches 95% of always-frontier’s promptfoo pass rate at 50% of its cost or less over 14 nightly runs, MoM defaults to mom-standard. It is the right bar: an independent evaluation found router gains “track selected-tier composition more closely than demonstrated task-specific targeting” (arXiv 2608.14641). A router has to beat a fixed tier, not simply exist.

After the model is chosen: canary, endpoint picker, autoscaling

MechanismWhat it routesKnobInterplay
LoRA canary10% of xplane-qwen-coder requests to the xplane-qwen-coder-sql-dpo adapter, on the same podgateway.canaries[].weightPercentThe adapter name is matched verbatim: a typo routes to nothing, with no error (LoRA canaries)
Endpoint pickerWhich replica, from KV-cache and queue stateenabled per claimMutually exclusive with canaries; worth it from two replicas up (roadmap)
KEDAHow many replicas a model getsmodel.maxNumSeqs, trigger thresholds 0.7 / 0.6 / 8Routing chooses whose queue grows. Every MoM default lands on xplane-qwen3-8b, so it saturates first. Its max: 2 and the pool’s 4-GPU cap bound it (autoscaling)

A routing change on the serving side is a capacity change too. Moving a category from xplane-qwen3-8b to xplane-qwen-coder moves its share of the batch-saturation trigger, and the LoRA canary shares the coder’s batch: adapter traffic scales the base model.

Agent side: tiers, data classes, names

How a run gets its model

    flowchart TD
  Iss["Task received"] --> Ctl{"controlPercent: 10<br/>this task in the control group?"}
  Ctl -->|"yes"| Fr["tier frontier"]
  Ctl -->|"no"| Cls{"classifier answered?"}
  Cls -->|"no: unbuilt today"| St["tier standard<br/>fallback: static"]
  Cls -->|"yes: design"| Tier["its tier, nudged up when unsure"]
  Fr --> Tpl{"template from class and tier"}
  St --> Tpl
  Tier --> Tpl
  Tpl -->|"docs-links below frontier"| Solo["solo"]
  Tpl -->|"other class below frontier, or docs-links at frontier"| Pair["pair"]
  Tpl -->|"other class at frontier"| Trio["trio"]
  Pair --> Rev["reviewer: the other tier<br/>standard after frontier, frontier otherwise"]
  Trio --> Rev
  Rev --> Name["model name: tiers.tier.model<br/>or templates.name.reviewerModel"]
  Solo --> Name
  Name --> DC{"spec.dataClass"}
  DC -->|"public"| G["GLM via Z.ai"]
  DC -->|"internal"| A["Claude via the Anthropic API"]
  

The aliases, identical on both Z.ai backends and mirrored on Anthropic’s (infrastructure/base/agent-gateway/llm.yaml):

Namepublic (Z.ai)internal (Anthropic)
tier-lightglm-5.3-flashclaude-haiku-5-5
tier-standardglm-5.3-flashxclaude-sonnet-5-5
tier-frontierglm-5.3claude-opus-5-5
agent-defaultglm-5.3claude-opus-5-5

Any other name is set to null by a CEL transformation and refused 400 before an upstream call (gate AG12). A run can still ask for any of the four names in its class (SP4 R10): the budgets bound what that costs.

Why every tier still asks for agent-default

# tooling/base/agent-factory/helm-values-configmap.yaml
tiers:
  light:    {model: agent-default, runTokens: 300000,  taskTokens: 600000,  runMinutes: 20, runUSD: 0.51, taskUSD: 1.02}
  standard: {model: agent-default, runTokens: 1500000, taskTokens: 3000000, runMinutes: 45, runUSD: 2.55, taskUSD: 5.10}
  frontier: {model: agent-default, runTokens: 4000000, taskTokens: 8000000, runMinutes: 90, runUSD: 6.80, taskUSD: 13.60}

When the factory was built the tier-* names answered 404 (plan R11). The gateway has served them on both listeners since AGW-8, so moving a tier to its own model is now a one-line change (see tuning). Nobody has measured whether glm-5.3-flashx or glm-5.3-flash can do a factory task, so the change has not been made. Until it is:

  • the tier sets caps, minutes and the team, not the model. The control group measures budget fit, not model fit;
  • the reviewer runs the implementer’s model. “The other tier” changes its caps only. ADR-0058 keeps it that way on public work, so no public data goes to Anthropic for review (ADR-0058).

Complexity routing for tasks: kept out of the request path

ChoiceDecisionRejectedWhy
Where to classifyOnce per task, at factory intake (C7)Per request in the data pathSwitching models mid-run loses the prompt cache and the tool state, and one under-routed step can fail a whole trajectory
Which classifierThe Semantic Router’s own complexity signal through POST /api/v1/eval: OSS firstLiteLLM’s complexity router; NeMo Switchyard; Jev in the pathOne thin service insulates the factory from router API churn (0.2 → 0.3 broke the config)
Jev (TypeSafe AI, SaaS)An optional adapter in shadow only, on dataClass: public text, never actingJev deciding tiersLatency, availability and data exposure; about $0.08 per 1,000 tasks if turned on

Source: agent factory design D8 and SP4 S6, §3. ADR-0047 is reserved for it and not yet written. None of it is built: on gcp-0 all 12 tasks read classifier: static, fallback: static.

What a call costs

Two price tables, and which one counts

TableWhereCache-awareUsed by
meter.pricestooling/base/agent-factory/helm-values-configmap.yamlyes: input, cacheRead, cacheWrite, outputThe run meter’s USD caps, and the gateway budgets B1, B2, B6, whose CEL cost string gate AG8 regenerates from it. This is the price that counts
llm_gateway:price_usd_per_mtokeninfrastructure/base/llm-gateway/vmrule-llm-gateway.yamlno: input and output onlyThe agent-run dashboard’s Cost (USD) panel and the AnthropicFleetNearCap alert

meter.prices, USD per million tokens, list prices read 2026-10-16:

ModelInputCache readCache writeOutputCache read vs input
glm-5.31.400.26not billed (priced as input)4.4019%
glm-5.3-flashx0.370.075not billed1.2520%
glm-5.3-flash0.150.03not billed0.5020%
claude-opus-5-54.000.205.0020.005%
claude-sonnet-5-52.000.102.5010.005%
claude-haiku-5-50.500.050.6252.5010%

Haiku is at its over-100K-prompt rate, which an agent’s context soon reaches. Over-counting is the safe direction for a cap.

The dashboard’s cost panel and AnthropicFleetNearCap overstate spend. They multiply input, cache reads included, by the full input price. At the measured mix that is about 4× the meter’s figure (see caching). The recording rule also lists claude-haiku-5-5 at 0.10 / 0.50, where meter.prices has 0.50 / 2.50. For money, read agent_factory_run_cost_usd or the Task’s costMicroUSD.

The meter’s formula

    flowchart LR
  GW["agentgateway<br/>agentgateway_gen_ai_client_token_usage"] -->|"VMPodScrape relabel<br/>input_cache_read to cached_input"| VM[("gen_ai_client_token_usage_sum<br/>by ar_agent, gen_ai_request_model,<br/>gen_ai_token_type")]
  VM -->|"meter.query, every 30 s"| M["run meter"]
  P["meter.prices<br/>keyed by served model"] --> M
  M --> F["fresh = input - cached_input - cache_creation_input<br/>cost = fresh x input + cached x cacheRead<br/>+ creation x cacheWrite + output x output"]
  F --> C1{"run cost at or over runUSD?"}
  C1 -->|"yes"| Rev["revoke: budget-run"]
  C1 -->|"no"| C2{"task cap still holds<br/>one whole run?"}
  C2 -->|"no"| Esc["escalate: budget-task"]
  F --> Met["agent_factory_run_cost_usd<br/>agent_factory_model_cost_usd_total<br/>Task status.runs.costMicroUSD"]
  
  • Keyed by the served model, not the logical name: agent-default is GLM-5.3 on one listener and Opus on the other. A model with no price row is never treated as free: its run falls back to its token caps and AgentModelUnpriced fires.
  • The input series includes cache tokens on Z.ai and on the non-streaming Chat Completions to Messages path, and excludes them on a streamed call or native Messages. The meter subtracts the cache tokens unless input is smaller than them, in which case it prices all of input as fresh (inputExcludesCache, agent-platform internal/factory/meter/meter.go).
  • Overshoot is one scrape plus one response. The run is revoked on the meter’s next tick, and calls already in flight complete.

Worked example: a real run

Run xplane-run-jcp6kl72, an implementer on a standard task, public, served by glm-5.3, which opened cloud-native-ref#2508. Its gateway counters on 2026-10-18:

SeriesTokens× priceUSD
input 4,433,376 − cached_input 4,161,472 = fresh271,904× 1.400.3807
cached_input4,161,472× 0.261.0820
output42,275× 4.400.1860
Run cost1.6487

The meter recorded costMicroUSD: 1648658 on the Task: an exact match. The same run under the old token cap would have been revoked at 1.5 M of its 4.48 M tokens, with the work unfinished. Priced with no cache discount it would show as $6.39.

The same token mix on other models, as arithmetic from meter.prices and not a measurement:

ModelCostvs glm-5.3
glm-5.3-flash$0.190.11×
glm-5.3-flashx$0.470.28×
glm-5.3$1.651×
claude-opus-5-5, cache writes excluded$2.771.7×
claude-opus-5-5, no cache$18.5811×

With caching, Opus is not ten times GLM. Opus’s cache read ($0.20) is cheaper than GLM-5.3’s ($0.26), so on a 94%-cached run the list-price gap narrows from about 3× on input to 1.7× overall. Anthropic does bill cache writes ($5.00 per million on Opus), which this estimate leaves out. The one measured internal run, wfeiraim (triager, Opus 5.5), cost $0.477 for 303,477 input tokens, 85% of them cache reads.

Budgets: five layers, one exact

    flowchart TB
  subgraph factory["Factory: exact, enforced"]
    R["run cap: runUSD<br/>0.51 · 2.55 · 6.80"] --> TK["task cap: taskUSD<br/>1.02 · 5.10 · 13.60"]
    TK --> D["daily: factoryDaily 25M, humanDaily 5M<br/>RAW tokens"]
  end
  subgraph gateway["agentgateway: backstop, in shadow"]
    B1["B1 per run: 5M ref tokens, $8.50"]
    B2["B2 fleet: 41M ref tokens, $69.70"]
    B6["B6 Anthropic: 10M ref tokens, $17"]
  end
  L["local: 40M raw tokens per hour per sub<br/>always enforced"]
  factory --> gateway --> L
  
LayerUnitEnforcedNote
runUSD, taskUSDUSD, cache-awareyesThe cap that decides a run’s fate
factoryDaily, humanDailyraw tokens: a cache read counts as a full tokenthe fleet’s day: tasks wait waiting_daily_budget. Per person: shadowSee the warning below
B1, B2, B6reference tokens: USD ÷ $1.70 per millionshadow (SHADOW_MODE: "true", infrastructure/base/agent-gateway/ratelimit.yaml)Sized above the factory’s caps, so the gateway never refuses what the factory admits
local limitraw tokens per hour, per replicaalwaysAbove the largest admitted run: $6.80 of Opus cache reads is 34M tokens
The daily budgets run out long before the money does. factoryDaily (25M) and humanDaily (5M) still count raw tokens. On 2026-10-18 both read 0 remaining (agent_factory_budget_remaining_tokens), and AgentFactoryDailyBudgetHigh was firing, after about $10 of real spend (29.1M raw tokens at a blended $0.357 per million). The raw-token alerts AgentRunTokenSpendHigh (5M in 8 h) and AgentFleetTokenSpendHigh fire on cache-heavy runs well inside their USD caps, as they did for d3jacb5z and kk2egjta that day. A USD (or reference-token) daily budget is the open fix.

Efficiency

Prompt caching is the largest lever

WhereCache reads / inputSource
gcp-0, all agent calls, 24 h to 2026-10-1895.1% (27.40M of 28.83M)gen_ai_client_token_usage_sum, live
Cohort arm A (GLM-5.3, factory)91–96%cohort results
wfeiraim, Opus 5.5 internal run85%plan, live evidence

On those 24 hours the meter’s figure is $10.40. The same tokens with no cache discount would be $41.64: caching cut the bill by 75%.

ProviderHow the cache engagesWhat to keep stable
Z.ai GLMAutomatic, no marker; cache writes not billedThe prompt prefix: system prompt, tool list, early turns
AnthropicExplicit breakpoints. The harness rewrites the SDK’s cache_control as prompt_cache_breakpoint, the only marker agentgateway 1.6 carries from Chat Completions to Messages (agent-platform images/agent-harness/site/sitecustomize.py)The same, and the breakpoint itself: an agentgateway upgrade that drops the field silently re-prices every internal run at the input rate

Caching is also why per-request model switching is a bad idea for agents: a new model starts with a cold cache, and on a 95%-cached run that multiplies the next call’s input cost by about 5.

Caps in USD, not tokens

The hard-task cohort found that the token cap, not the work, decided 11 of the factory’s 15 issues: it counted every cache read as a full token, and runs stopped at about 1.5M tokens and $0.58–0.76. The owner’s decision (OD-6, #2369) moved the caps to USD. Today’s live record shows the change at work:

Run (standard, $2.55 cap)TokensMeter USDUnder the old 1.5M token capOutcome
jcp6kl724.48M$1.65revoked at a third of its workfinished, PR opened
d3jacb5z7.42M$2.56revoked far earlierrevoked at its USD cap: budget-run, fit under

Cheaper tiers: priced, not measured

Moving light and standard to their own names would cut the price of the same tokens to 0.11× and 0.28× (worked example above). Whether those models finish factory tasks is unmeasured. The one model comparison on record points the other way: on harder tasks the gap followed the model.

ArmModelAcceptedUSDUSD per accepted
A, the factoryGLM-5.30 of 159.00n/a
B, Claude Code ActionClaude13 of 1515.801.22
C, Claude Code ActionGLM-5.30 of 1525.65n/a

C ran B’s harness on A’s model and hit the 60-turn cap on all 15 issues, at $1.13–2.33 each. A cheaper model that fails costs more per accepted outcome than a dearer one that succeeds. The cohort’s caveat holds: fifteen issues separate only large differences, and A was stopped by its old token cap.

Self-hosted against an API

QuestionAnswer from the repository
Is self-hosting a cost play?No. Four always-warm L4 models are about $876 a month on spot ($0.30 an hour each, the May design’s AWS estimate, docs/superpowers/specs/2026-05-06-oss-llm-foundation-showcase-design.md). That buys about 84 days of the agents’ 2026-10-18 GLM-5.3 spend, or about 2.4B tokens a month at its blended $0.357 per million
Can the local fleet serve agents?No, by design. “Local 7–8B models are never agent tiers” (SP4 §5): agents need frontier capability with zero GPUs
Why self-host at allPrompts that never leave the tailnet, a model pinned to a commit, sub-200 ms tab completion on a warm replica (why self-host)
When does the trade changeThe serving roadmap triggers: a bigger coder when the 7B hits its limits, L40S or tensor-parallel when quantized quality loss becomes measurable, cost per tenant when several consumers share the platform

The scale-to-zero deadlock is why the floor is fixed: a model with no pod emits no metric to wake it (autoscaling).

Tuning

Every knob, what it trades and how to see the effect. Factory values live in tooling/base/agent-factory/helm-values-configmap.yaml, a gate path a maintainer changes. A meter.prices change must be mirrored in the CEL cost of infrastructure/base/agent-gateway/policy-budgets.yaml: gate AG8 fails any difference.

GoalKnobTrade-offMeasure with
Cheaper agent runstiers.<tier>.model: tier-standard or tier-light0.28× or 0.11× the price; quality unmeasuredagent_factory_tier_fit_total, agent_factory_pr_outcomes_total, cost per merged task
Better hard-task outcomesdefaults.dataClass or the task’s class, internal → OpusData class first, cost second: only internal data may reach Anthropic (ADR-0054)the cohort’s measure: accepted outcomes and USD per accepted
A reviewer independent of the implementertemplates.pair.reviewerModelAnother size only on public: ADR-0058 rejected a Claude reviewer on data class and cost, and no other open-weight family has a routereview verdicts against maintainer rejections
Measure model fit, not only budget fittriage.controlPercent (10)More control tasks run frontier with trio, at about 2.7× the run capagent_factory_tier_fit_total{fit}
Fewer runs revoked mid-worktiers.<tier>.runUSD, taskUSDA run cap above $6.80 also needs B1 and the local limit raised: AG8 checks itagent_factory_run_revocations_total, budget-run reasons
Daily budget not spent by cache readsbudgets.factoryDaily, humanDaily (raw tokens)Raising them loosens the only daily stop until a USD daily cap existsagent_factory_budget_remaining_tokens
Gateway budgets that refuseSHADOW_MODE: "false" in ratelimit.yaml429 agent_budget_exhausted; wait for a longer shadow record (status)agent_router:budget_shadow_overrun_tokens:increase10m
Lower chat latencyClients name the model instead of MoMLoses automatic choice; saves 250–300 msgateway TTFT on LLM Gateway
Less chain-of-thought spend in chatTrim reasoning_keywords, raise long_query.min_tokensFewer use_reasoning: true decisions; weaker answers on multi-step promptsoutput tokens per model, promptfoo pass rate
Better coding answers in chatWiden code_keywords or the code categoriesMore traffic, and more KEDA pressure, on the codervllm:num_requests_running per model
Adapter rolloutgateway.canaries[].weightPercentMore users on a less-proven adapter; no endpoint picker while a canary existsLLM Gateway base vs canary panels, canary SLO alerts
Throughput per GPUmodel.maxNumSeqsAlso raises the scale-up bar: trigger 1 divides by itvllm:num_requests_waiting, KV-cache usage

Move a tier to its own model:

# tooling/base/agent-factory/helm-values-configmap.yaml
tiers:
  # tier-light is glm-5.3-flash on public, claude-haiku-5-5 on internal (llm.yaml, anthropic.yaml);
  # both are priced in meter.prices, so the USD caps keep working.
  light: {model: tier-light, runTokens: 300000, taskTokens: 600000, runMinutes: 20, runUSD: 0.51, taskUSD: 1.02}

Do it with a measurement in place: run a cohort on the tier with fixed-tier arms, as the SP4 measurement rule asks, and compare accepted outcomes and USD per accepted outcome.

Change a provider model behind a name (both Z.ai backends, and the gate’s price row):

# infrastructure/base/agent-gateway/llm.yaml, on zai and zai-anthropic
modelAliases:
  tier-standard: glm-5.3-flashx   # a new model here needs its meter.prices row, or AG10 fails

In-flight runs see a mapping change on their next request. Use the factory’s pause first when that matters.

Tune the chat cascade (Semantic Router 0.2 schema):

# infrastructure/base/vllm-semantic-router/helmrelease.yaml
context_rules:
  - name: long_query
    min_tokens: 500    # raise to turn reasoning on for fewer code prompts
    max_tokens: 32000

The complexity signal and role targets need SR 0.3.0 (SP4 PR 4). Do not add a complexity rule to the 0.2 config with the documented 0.70–0.75 threshold: it never fires.

Measuring

QuestionQuery or panel
Tokens by served model and typesum by (gen_ai_request_model, gen_ai_token_type) (increase(gen_ai_client_token_usage_sum{namespace="agent-gateway"}[24h]))
Cache-read sharesum(increase(gen_ai_client_token_usage_sum{namespace="agent-gateway",gen_ai_token_type="cached_input"}[24h])) / sum(increase(gen_ai_client_token_usage_sum{namespace="agent-gateway",gen_ai_token_type="input"}[24h]))
USD per model, as the meter priced itsum by (model) (agent_factory_model_cost_usd_total)
USD per run, by tieragent_factory_run_cost_usd_sum / agent_factory_run_cost_usd_count, by tier
One run’s cost, exactlykubectl get task <id> -n agent-system -o jsonpath='{.status.runs[*].costMicroUSD}'
Did the tier fit?agent_factory_tier_fit_total{classifier, tier, fit}: under, fit, over
Model latencyhistogram_quantile(0.95, sum by (le, gen_ai_request_model) (rate(gen_ai_server_request_duration_seconds_bucket{namespace="agent-gateway"}[1h]))): p50 2.8 s, p95 18.4 s on glm-5.3, 2026-10-18
What enforcing B1–B2 would refuseagent_router:budget_shadow_overrun_tokens:increase10m
Daily budget leftagent_factory_budget_remaining_tokens / agent_factory_budget_cap_tokens by principal
Serving: which model is hotvllm:num_requests_running, vllm:kv_cache_usage_perc, vllm:num_requests_waiting by model_name
Dashboard (uid)Read it for
agent-gatewayTokens by type as the meter reads them, tokens per agent, B1–B2 counted and would-refuse, model latency
agent-factoryTokens per finished task p90, budget left today, budget fit by classifier, class mismatches
agent-fleetTokens per run, tier against tokens and steps, tokens by tier
agent-runOne run: tokens against budget, model calls. Its Cost (USD) panel overstates; see costs
LLM Gateway — Envoy AI Gateway GenAI Telemetry, LLM Platform — Self-hosted vLLM FleetServing: tokens per model, base against canary, TTFT, queue and KV cache. Folder llm, present only while llm-platform runs

Signal definitions and alerts are on Observability.

Known gaps

GapEffectCloses with
No triage classifierEvery task is standard; the tier never reflects difficultySP4 PR 5
Every tier is agent-defaultTiers change caps, not models; the reviewer shares the implementer’s modelA measured tier move
Daily budgets in raw tokensThe fleet’s day runs out after about $10 at today’s cache mixA USD daily cap
Two price tables disagreeDashboard cost and AnthropicFleetNearCap overstate; Haiku is mispriced in the recording ruleOne table, from meter.prices
The router fails closed, on one replicaA router outage fails every ai-gateway request, tier-frontier includedSP4 PR 4
PII classifier loaded, read by no decisionNo data-class protection on MoM today. Harmless while every MoM target is localSP4 PR 4 (NOT pii_any on the frontier decision)
No Semantic Router series in VictoriaMetrics on gcp-0The router’s decisions cannot be read from metrics todayNot investigated