Rank tiers by capability, verify every rung is callable, route scout and vision - #37
Merged
Conversation
Input cost stopped being a proxy for capability once providers began
repricing new models below predecessors they never delisted. Run against
today's real catalog, the old scaffolder produced:
plan/slow/designer/reviewer -> claude-opus-4-1:high ($15, 200k, xhigh)
fallback -> claude-opus-5:high ($5, 1M, max)
the smart rung led by a fossil its own fallback dominates on every axis.
At the top dial position it also capped thinking, emitting
`claude-opus-4-1:xhigh` while the model behind it could do `max`.
The ladder is now derived rather than guessed from price:
- supersession first: only the newest member of each model family
survives, so claude-opus-4-1 never competes with claude-opus-5.
Version components compare numerically, so a future 5.10 beats 5.9.
- ranking by thinking ceiling, then context, then price. Price is the
last word, not the first: among models with identical published
specs it is the only remaining signal of size, but across
generations it lies.
- loadCatalog now asserts the property the ranking should produce. A
dearer rung offering less context or less thinking headroom is
rejected with both models named, rather than silently shipped.
Tiers 0 and 4 were previously unreachable — pickLadder only ever emitted
tiers 1..3, so the spark and fable dials were dead on every scaffolded
config and the shipped default (spark on) opened the TUI on a combo that
was never generated. They are now read from the tier-scoped quota buckets
omp already reports, capped at one per pool and sanity-checked against
price.
Also:
- --bench fills speed/ttft from `omp bench --json` instead of writing
50/2.0 for every model, which made the speed meter model-invariant.
It doubles as a reachability probe: omp lists claude-mythos-5 at
claude-fable-5's exact price but it 404s here, and no metadata
distinguishes them. A failed probe drops the model.
- --refresh re-derives tiers over an existing file. init used to refuse
outright, which is how a catalog goes three model generations stale
without anyone noticing.
- thinking is a level set, not a range. claude-opus-4-6 offers
low/medium/high/max but not xhigh; a range claimed a level the API
rejects.
- same-family short keys disambiguate by version (opus5, opus48)
rather than by ladder index, which named the newer model `opus` and
the older one `opus3`.
The models file gains bucket, context and image fields. The init fixture
now models the catalog's real shape — four same-priced Opus variants, an
undelisted $15 legacy, two identically priced elites, a text-only spark —
so it can actually fail on the bug it is meant to guard.
omp bundles six agents; the grid routed five. `scout` — the one its own prompt marks "MUST be used for exploratory codebase research" — silently inherited @smol and never appeared in the preview, so its model was neither visible nor selectable. It is now an agent-backed role, which means genConfigYAML mirrors it into task.agentModelOverrides for free. `vision` backs omp's image-describe fallback (describeForTextModels is on by default) and always leads on a model that accepts images, since the codex spark variants are text-only. Both needed roleWeight entries: weightedModels silently skips a role it cannot weigh, so an omission drops that model out of the cost and speed meters with no trace. A test now asserts every emitted role is weighted. Enter no longer launches a combo the catalog does not carry. It walked a nil block and emitted an overlay whose modelRoles map was empty, handing omp a session with no routing at all — reproducible today on a stock scaffold, where the shipped default (spark on) points at a combo no tier-0-less catalog contains. The dials themselves are now derived from the catalog: a toggle with no combos is forced off and hidden rather than offered. The quota bucket a model draws is read from the catalog's new column, falling back to the family-substring guess only when a catalog declares none. Names are not a taxonomy — claude-mythos-5 sits at claude-fable-5's price yet 404s on this account, and every model omp adds would otherwise need one more substring arm before it could be struck through correctly.
- docs/configuration.md documented CODE_USAGE as the usage panel's source; nothing reads it, and the panel has been broker-sourced since the account-pool work. Removed, with a note that the wrapper still exports it for older pins so a reader who greps it is not misled. Seven vars the binary does read were undocumented: the three OMP_AUTH_BROKER_*, CODE_AUTH_VAULTS(_FILE), CODE_AUTH_ACCOUNT_STATE and CODE_USAGE_CACHE. - README claimed the first run walks you through building a catalog. True for a plain install; the dotfiles wrapper always exports CODE_GENERATED, so onboarding cannot trigger there. - docs/status.md's two honest gaps are narrowed rather than dropped: speed/ttft placeholders now have --bench, and the quota bucket is a catalog fact with the family guess as fallback. - Documents the generate subcommand, which the README linked to but the page never covered.
omp lists models an account cannot call, and nothing in the metadata says which. `claude-mythos-5` reports `claude-fable-5`'s exact price, context window and thinking range, and 404s here. Until now the only thing keeping it off the ladder was a shorter-id tiebreak — rename it `claude-myth-5` and it wins the smart rung on this account, and every profile leading with it fails at launch. So reachability is no longer a flag. `init` probes every candidate and only a model that answers may become a rung; the file is stamped `probed: true`, and `code generate` refuses one that is not. That marker is the whole gate: `--from-json` still scaffolds offline, but stamps `false`, so an unverified scaffold cannot quietly become live routing. `--bench` is gone — its measurement is now a side effect of a probe that always runs, which also retires the placeholder speed/ttft pair that made the speed meter identical for every model. A failed request is not the same as an absent model, and conflating them is expensive. Probing the real catalog, `claude-fable-5` failed: omp's bundled bench prompt trips Anthropic's safety layer, and the model refuses it. A rule of "any failure means unreachable" would have silently deleted a perfectly callable elite from the ladder. The probe therefore asks something innocuous, and sorts each model three ways: reachable answered — eligible, and measured not found the provider says it does not exist — dropped unresolved refusal, rate limit, missing row — nothing is inferred Unresolved is a refusal to certify, not a quiet drop: `init` names each model and its reason and writes nothing, because a `probed: true` file built from a partial probe is worse than no file. Second defect, same shape: the elite tier was read only from omp's tier-scoped quota buckets, and that report turns out to depend on the ambient auth environment — the anthropic fable bucket is visible from one environment on this machine and absent from another, minutes apart. When it was absent the elite did not merely go undetected, it fell back onto the ordinary ladder and was crowned tier 3, so every routine "smart" request would drain the scarce bucket that exists to be spent deliberately. Price now confirms what the bucket used to assert alone: a pool's top model priced at twice the next is its elite, bucket or no bucket. Price says nothing about entitlement — that is the probe's job — but a model in its own price class is not the everyday workhorse. Verified against the live catalog: 21 models probed in ~47s, claude-mythos-5 dropped, claude-fable-5 kept and correctly crowned tier 4, yielding haiku/sonnet-5/opus-5/fable-5 and spark/mini/terra/sol.
The catalog header names the roles the ● marker can appear on. It was a hand-typed list sitting beside genAgentRoles, so routing scout marked 414 rows the header denied existed. Derived from genAgentRoles in genRoleOrder, so the legend now reads in the same order as the rows beneath it and cannot drift again. A test asserts the legend and the marked rows name the same set - nothing covered that line before, which is why it went stale unnoticed.
atyrode
force-pushed
the
model-selection-redesign
branch
from
July 25, 2026 13:07
ca95e04 to
60f27f7
Compare
This was referenced Jul 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Driven by Opus 5 shipping. The id swap was the smallest part.
1. Price stopped predicting capability
pickLadderranked by input cost. Against today's live catalog that produced:The smart rung led by a fossil its own fallback dominates on every axis, capped a thinking rung below what the model behind it offers. Providers now reprice successors below predecessors they never delist, so cost inverts capability.
Now: supersession first (newest per family, version components compared numerically so a future
5.10beats5.9), then ranked by thinking ceiling → context → price. Price is the last word, not the first.loadCatalogasserts the result: a dearer rung offering less context or less thinking headroom is rejected by name.2. A model your account cannot call could lead the smart rung
This is the part worth reviewing closely.
omp models --jsonlistsclaude-mythos-5. It 404s on this account. It reportsclaude-fable-5's exact price, context window, thinking range, modalities and maxTokens — I diffed every field; onlyid/name/selectordiffer. Nothing in the metadata says which one you can call.The only thing keeping it off the ladder was a shorter-id tiebreak. Rename it
claude-myth-5and it wins the smart rung, and every profile leading with it fails at launch.So reachability is no longer a flag.
initprobes every candidate; only a model that answers may become a rung. The file is stampedprobed: trueandgeneraterefuses one that is not — that marker is the gate, so--from-jsoncan still scaffold offline (stampedfalse) without an unverified scaffold quietly becoming live routing.--benchis gone: measurement is now a side effect of a probe that always runs, which also retires the placeholder50/2.0pair that made the speed meter identical for every model.The probe found a false positive that mattered.
claude-fable-5failed — not unreachable, but refusing omp's bundled bench prompt:A rule of "any failure means unreachable" would have silently deleted a perfectly callable elite from the ladder.
omp benchis not a reachability oracle on its default prompt. So the probe asks something innocuous (--prompt), and sorts three ways:Unresolved is a refusal to certify, not a quiet drop:
initnames each model and its reason and writes nothing. Aprobed: truefile built from a partial probe is worse than no file.3. The elite was crowned as the everyday smart rung
Tier 0/4 came only from omp's tier-scoped quota buckets. That report turns out to depend on the ambient auth environment — the
anthropic:fablebucket is visible from one environment on this machine and absent from another, minutes apart, deterministically (3/3 each way). Root cause:OMP_AUTH_BROKER_TOKEN/_URLpresent or not.When absent, the elite didn't merely go undetected — it fell onto the ordinary ladder and became tier 3, so every routine "smart" request would drain the scarce $10 bucket that exists to be spent deliberately. Observed live:
Price now confirms what the bucket used to assert alone: a pool's top model priced at twice the next is its elite, bucket or no bucket. Price says nothing about entitlement — that is the probe's job — but a model in its own price class is not the everyday workhorse.
4. scout and vision
omp bundles six agents; the grid routed five.
scout— the one omp's own prompt marks "MUST be used for exploratory codebase research" — silently inherited@smol, invisible in the preview and unselectable. Now agent-backed, sogenConfigYAMLmirrors it intotask.agentModelOverrides.visionis one of omp's ten real role ids (confirmed against omp's docs) and backs image description when the active model is text-only; it always leads on an image-capable model, since the codex spark variants are not.Both needed
roleWeightentries —weightedModelssilently skips a role it cannot weigh, dropping it from both meters with no trace. A test asserts every emitted role is weighted.Routing scout also desynced the catalog header, which carried a hand-typed copy of the agent list beside
genAgentRoles: the grid marked 414● scoutrows while the legend named five agents and denied scout existed. The legend is now derived fromgenAgentRoles, and a test asserts it names exactly the roles the rows mark — nothing covered that line before, which is how it went stale unnoticed.Enter no longer launches a combo the catalog lacks. It walked a nil block and handed omp an empty
modelRoles:map — reproducible on a stock scaffold, where shippedspark: onpointed at a combo no tier-0-less catalog contained. Dials are now derived from the catalog: a toggle with no combos is forced off and hidden.Result
The hand-curated dotfiles ladder, derived automatically. 414 combos (was 91, none with spark or fable).
Verification
go build,go vet,gofmt,go test ./...clean — 9.4s, down from 564s: the suite had been making real API calls throughobScan. Now fully hermetic.code generate init: 21 models probed in ~47s → mythos dropped, fable-5 retained and correctly tier 4,probed: truewritten.414 x 2lines — the newscoutandvisionrows, nothing else.TestScaffoldRefusalSurfacesNotDropped— a refusal must surface as unresolved naming the model, while a genuine 404 is dropped and must not appear in that list. It fails under the previous two-state rule.mainaftercode ls/session reap(feat(session): add code ls and code session reap #36) and the v0.4.8 pin (chore(nix): pin code v0.4.8 #38) landed — clean, no conflicts. Re-verified on the combined tree: build/vet/fmt/tests green,code lsworks, and the generator still renders the dotfiles catalog to 415 combos.Not in this PR
No release tag — per
AGENTS.mdav*tag is a deployment, so that waits for review. Companion: atyrode/dotfiles#341.One loose end, evidenced but unfixed:
retry.fallbackChainskeyed by agent name never fire. Verified by forcing a retry — asonic:chain specifying haiku was ignored and the agent landed on sonnet-5 instead, becausereviewer/librarian/sonic/scoutare agent names, not role ids. Role-keyed chains do work (adefault:chain fired exactly as written). That affectsdefaults.yml, not this repo, and keying by model selector shadows the workingdesigner:chain, so it needs a decision rather than a patch.