Young's Mind
03 Decisions

Dynamic Model Router

Consolidated. This note's findings now live in ../MIN-STUDIES.md §7. The original is kept for its measurements; do not re-derive from it.

The dynamic model router

Young, 2026-10-10: "We need a dynamic models enable for free / paid models — available and ready for agents to use based on their task."

This is the right shape, and it is the same pattern as the skills load model. An agent does not get the model; it gets the model that fits its task, chosen from what is live right now, free first.


1. The two sources, and which wins

I found two registries, and they DISAGREE. That disagreement is the design's first constraint.

SourceWhat it givesAuthoritative for
GET /zen/go/v1/models · GET /zen/v1/modelswhich models exist, on which endpointexistence + namespace — this is live
https://models.dev/api.jsonprice, context limit, modalities, reasoning, tool-call per modelcapability + cost metadata
opencode models (CLI)84 models across 4 providers, locallywhat this install can reach

🔴 They disagree, and the live endpoint wins

Measured 2026-10-10:

models.dev opencode-go : 36 models
LIVE   /zen/go/v1/models: 38 models

in models.dev but NOT live : grok-4.5, space-bunny-free
live but NOT in models.dev : deepseek-flash, glm-5.1, minimax-m2.5, omen-alpha

And the space-bunny-free case is the same defect, from the other side:

SourceSays space-bunny-free is on
models.devopencode-go ❌
LIVE /zen/go/v1/modelsnot present
LIVE /zen/v1/modelspresent ✅
our configlab-go (= GO base) ❌

So models.dev carries the SAME wrong namespace that our config had. Two independent sources made the same mistake — which means the mistake is easy to make and only the live endpoint can settle it.

The rule: existence and namespace come from the LIVE endpoint, always. Price and capability come from models.dev. Join on model id. The live endpoint wins every conflict.


2. What the router needs, per candidate

Every input exists — nothing here is speculative:

InputSourceExample (GLM-5.3-Flash)
id/modelsglm-5.3-flash
endpointwhich /models answeredzen/go/v1
free or paidmodels.dev cost.input == 0paid
input / output / cache pricemodels.dev cost$0.15 / $0.50 / $0.03
context limitmodels.dev limit.context1,000,000
output limitmodels.dev limit.output131,072
modalitiesmodels.dev modalitiestext, image, video, pdf
reasoning capablemodels.dev reasoningtrue
tool-call capablemodels.dev tool_calltrue
monthly allowancethe docs table$60
window state/zen/go/v1/usageok
measured healthV2's prober patternalive / dead

And the router must reject a candidate that cannot do the task — a model without tool_call cannot be given a tool-using task, regardless of price.


3. The selection ladder

Free first, then paid, and within a tier by limit ÷ price — the rule from go-allowance-and-model-selection.md.

1. LIVE list      → which models exist, on which endpoint
2. CAPABILITY     → drop any that cannot do the task (tool_call, modality, context)
3. HEALTH         → drop any measured dead
4. WINDOW         → drop any whose window is `rate-limited`
5. FREE FIRST     → if a free model passes 1–4, use it. Done.
6. PAID, RANKED   → by (monthly_limit ÷ effective_price) on the task's mix
7. ATTRIBUTE      → record endpoint, model, instrument, window_state

Step 5 is where the win is. Measured today: ZEN has 8 free models, GO has 2. A task that a free model can do never touches an allowance.

Step 6 is where the last finding pays off: on a coding-agent mix, Muse Spark 1.2 Contributor gives 2,765 units vs GLM-5.3-Flash's 916 — ~3× the work from the same $60.


4. "Based on their task" — what that means concretely

The task declares what it needs; the router matches. Same shape as the skills search: declare, search, load.

Task declaresRouter requires
capability: codetool_call == true
needs: long_contextlimit.context >= task size
needs: vision"image" in modalities.input
needs: reasoningreasoning == true
retention: zeroonly a zero-retention model — Stage 6.4
budget: free_firstthe ladder above

So the agent does not pick a model — it states a requirement, and the router resolves it against what is live. That is what makes it dynamic: the answer changes when the free list changes, and nobody edits a file.


5. 🔴 The namespace lesson — from V2, and it applies exactly here

V2's router has this written into it, and it cost a day:

"NAMESPACE — this is the whole reason it looked dead all day. Measured 3/3 DEAD in opencode/, 3/3 ALIVE in opencode-go/, 3/3 DEAD in opencode-zen/. The breaker and the prober were probing the Zen form and correctly quarantining it, while the live GO form went untested. That is what 'flapping' was: one namespace, two verdicts, and the dead one was the one we were watching."

A model is (endpoint, id), never the id alone. V2 learned this by watching a healthy model get quarantined because the probe used the wrong namespace.

So the router's key is the pair, and every health record, every window read and every ledger row carries both halves.


6. What to adopt, and what to build

PieceSourceVerdict
capability-based routing, never hardcodedV2 bin/route_model.pyadopt the shape — route({"capability":"code","budget":"free_first"})
the tier ladder — a tier is used only if every cheaper tier has no candidateV2adopt verbatim — it is the free-first rule, already written
the namespace ruleV2adopt verbatim — it is a scar, not a preference
the health proberV2 bin/model_prober.pyadopt — it times models, so ranking is measured not guessed
models.dev metadataHermes agent/models_dev.pyadopt — price, context, modalities, capability; cached with ETag, 4 h TTL
the live model list/zen/go/v1/models, /zen/v1/modelsadopt — existence + namespace, no-store
the window readout/zen/go/v1/usageadopt — per-model gate
the live rostercortex/registry/agents.yamlextend — agents declare a default model, not a fixed one

What V2 got wrong, and we do not repeat: its PAID_ALLOWLIST was empty and its GO models sat in a QUOTA_TIER dict — a hand-written list in the router, which is the saved-list defect again. The fix is the same as the model list: derive it.


7. The proof of the stage

A task declares a capability; the router returns a live model that can do it; and the answer CHANGES when the free list changes — without editing a file.

Stated as a probe:

  1. Ask the router for capability: code → it returns a model that is in the live list and has tool_call: true.
  2. Add a fake free model to the live list → the router prefers it.
  3. Mark the chosen model's window rate-limited → the router moves to the next candidate.
  4. Remove a model from the live list → the router stops offering it.

Probe 4 is the one that would have caught space-bunny-free.


8. Where it lives, and what it must not do

Owner: Mira — it is a cost decision, and the ladder is a spend rule. With Sol — the executor consumes the answer, and the namespace rule is his seam.

Must NOT doWhy
Hold a saved model listthe provider says no-store; the list moves weekly
Hardcode a capability→model mapV2's PAID_ALLOWLIST was empty and its GO models were a hand-written dict — the defect again
Route a zero-retention task to an undocumented modelStage 6.4 — retention outranks cost
Route outside OpenCodemeasured: the free tier is OpenCode-client-only (FreeTierError 403)
Trust models.dev's namespacemeasured: it has space-bunny-free on GO. It is wrong

9. Not established

  • models.dev was read, not schema-verified — 226 providers, 5.2 MB; I read opencode-go and opencode only.
  • The 84-model opencode models list was not reconciled against the live endpoints per provider — the counts differ by construction (it spans 4 providers).
  • No routing decision has been executed. Every input is verified present; the ladder has not run.
  • The health prober was not run. V2's model_prober.py exists; its current verdicts are unknown.
  • The monthly-allowance join is by model id against a docs table — the one input that is documented rather than derived.
Copyright © 2026