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.
| Source | What it gives | Authoritative for |
|---|---|---|
GET /zen/go/v1/models · GET /zen/v1/models | which models exist, on which endpoint | existence + namespace — this is live |
https://models.dev/api.json | price, context limit, modalities, reasoning, tool-call per model | capability + cost metadata |
opencode models (CLI) | 84 models across 4 providers, locally | what 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:
| Source | Says space-bunny-free is on |
|---|---|
models.dev | opencode-go ❌ |
LIVE /zen/go/v1/models | not present |
LIVE /zen/v1/models | present ✅ |
| our config | lab-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:
| Input | Source | Example (GLM-5.3-Flash) |
|---|---|---|
| id | /models | glm-5.3-flash |
| endpoint | which /models answered | zen/go/v1 |
| free or paid | models.dev cost.input == 0 | paid |
| input / output / cache price | models.dev cost | $0.15 / $0.50 / $0.03 |
| context limit | models.dev limit.context | 1,000,000 |
| output limit | models.dev limit.output | 131,072 |
| modalities | models.dev modalities | text, image, video, pdf |
| reasoning capable | models.dev reasoning | true |
| tool-call capable | models.dev tool_call | true |
| monthly allowance | the docs table | $60 |
| window state | /zen/go/v1/usage | ok |
| measured health | V2's prober pattern | alive / 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 declares | Router requires |
|---|---|
capability: code | tool_call == true |
needs: long_context | limit.context >= task size |
needs: vision | "image" in modalities.input |
needs: reasoning | reasoning == true |
retention: zero | only a zero-retention model — Stage 6.4 |
budget: free_first | the 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 inopencode-go/, 3/3 DEAD inopencode-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
| Piece | Source | Verdict |
|---|---|---|
| capability-based routing, never hardcoded | V2 bin/route_model.py | adopt the shape — route({"capability":"code","budget":"free_first"}) |
| the tier ladder — a tier is used only if every cheaper tier has no candidate | V2 | adopt verbatim — it is the free-first rule, already written |
| the namespace rule | V2 | adopt verbatim — it is a scar, not a preference |
| the health prober | V2 bin/model_prober.py | adopt — it times models, so ranking is measured not guessed |
models.dev metadata | Hermes agent/models_dev.py | adopt — price, context, modalities, capability; cached with ETag, 4 h TTL |
| the live model list | /zen/go/v1/models, /zen/v1/models | adopt — existence + namespace, no-store |
| the window readout | /zen/go/v1/usage | adopt — per-model gate |
| the live roster | cortex/registry/agents.yaml | extend — 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:
- Ask the router for
capability: code→ it returns a model that is in the live list and hastool_call: true. - Add a fake free model to the live list → the router prefers it.
- Mark the chosen model's window
rate-limited→ the router moves to the next candidate. - 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 do | Why |
|---|---|
| Hold a saved model list | the provider says no-store; the list moves weekly |
| Hardcode a capability→model map | V2'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 model | Stage 6.4 — retention outranks cost |
| Route outside OpenCode | measured: the free tier is OpenCode-client-only (FreeTierError 403) |
Trust models.dev's namespace | measured: it has space-bunny-free on GO. It is wrong |
9. Not established
models.devwas read, not schema-verified — 226 providers, 5.2 MB; I readopencode-goandopencodeonly.- The 84-model
opencode modelslist 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.pyexists; 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.
The loophole — and how to close it
Young: "Look like the .env on opencode CLI already included on LAB and production. Heart agents just connected and execute on it with their preferred models. This a loophole need to address and how to keep track with spending. Work with Mira on it."
Fork Moyai? — three-lane consensus
Young, 2026-10-10: "Moyai and liteagents, they're existed and ready for production. SolOSV3 is reinventing the wheel that they both included? Can we fork from Moyai and add SolOS V3 touch on it?"

