fix: unstick the model alias from a superseded Opus, bump the agent SDK - #315
Merged
Conversation
Two coupled problems, both invisible until you asked for a model by name.
1. `opus` resolved to claude-opus-4-8, months after Opus 5 shipped.
`resolveAgainstList` matched a live entry by exact `value` or exact
`displayName`. The backend reports the Opus entry as value `opus[1m]`,
displayName "Opus (1M context)" — so the bare alias `opus` matched neither,
returned null, and fell through to the baked-in MODEL_CATALOG, which still
pinned `opus` to claude-opus-4-8. The live-list-wins design was sound; the
alias just never reached it.
Match on the value with its bracketed variant suffix stripped, so `opus`
resolves to `opus[1m]` and the live list wins as intended. Exact matches are
checked across the whole list first, so a backend offering both `opus` and
`opus[1m]` still resolves the exact entry. The catalog's premium alias moves
to claude-opus-5 so the pre-first-report fallback isn't stale either.
models.test.ts already asserted `opus` -> `opus[1m]`, and passed — its
fixture claimed the entry's displayName was "Opus", which made it resolve by
a display-name match that does not exist in the real payload. The fixture is
now verbatim from supportedModels() on 0.3.258. That fiction is why a bug
survived a test that looked like it covered it.
2. The pinned SDK could not run Fable 5.1 at all.
claude-agent-sdk 0.3.220 bundles Claude Code 2.1.220, and claude-fable-5-1
returns `400 ... version 2.1.251 or newer is required`, then silently falls
back to Haiku — a wrong-model completion with no error surfaced to the user.
0.3.258 bundles 2.1.258 and runs it. The SDK version tracks the bundled CLI
1:1, so the dependency bump *is* the CLI bump; the manifest floor moves with
it because 0.3.220 cannot serve the model, which makes it a requirement
rather than a preference.
Verified against the live backend on the upgraded dependency:
live list: default, opus[1m], claude-fable-5-1[1m], sonnet, haiku
resolveAgainstList("opus") -> opus[1m]
resolveAgainstList("fable") -> claude-fable-5-1[1m]
resolveModelId("opus") -> claude-opus-5 [fallback path]
fable-5-1 turn: is_error=false model=claude-fable-5-1
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
KunalJavelin
approved these changes
Sep 2, 2026
akhiljavelin
approved these changes
Sep 2, 2026
rsharath
approved these changes
Sep 2, 2026
jalbrethsen-highflame
approved these changes
Sep 2, 2026
saucam
added a commit
that referenced
this pull request
Sep 3, 2026
…316) `/model opus` was rejected in the web UI as an unknown model while the daemon behind it resolved the very same input fine. Model-value resolution existed twice: `resolveAgainstList` in src/daemon/models.ts and `resolveModelInput` in web/src/state/models.ts, the latter a hand-maintained mirror. They drifted. #315 taught the daemon copy to match a bare alias against a variant-suffixed value (the backend reports `opus[1m]` / "Opus (1M context)", so a typed `opus` matches neither field verbatim) and the mirror kept the old rule — so the client rejected the input before the request was ever sent. Patching the mirror would leave two copies and the same drift. The resolver moves to @highflame/codeoid-core instead, which already exists for exactly this (see the re-export shims in web/src/lib/{approvals,identity,format}.ts): packages/core/src/models.ts — the one implementation src/daemon/models.ts — re-exports it, 37 lines of duplicate deleted web/src/state/models.ts — resolveModelInput delegates to it Behaviour is unchanged on the daemon side and now matches it on the client. The web client keeps its own pre-catalog passthrough: with an empty list the trimmed input goes through untouched so typing isn't blocked before the fetch lands, and the daemon stays the backstop validator. Coverage moves to packages/core alongside the code, with the live payload from claude-agent-sdk 0.3.258 as the fixture, plus a web-side regression test that drives the real symptom through fetchModels -> resolveModelInput. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Two coupled problems, both invisible until you ask for a model by name.
1.
opusresolved to Opus 4.8, months after Opus 5 shippedresolveAgainstListmatched a live entry by exactvalueor exactdisplayName. The backend reports the Opus entry as:{"value": "opus[1m]", "displayName": "Opus (1M context)"}So the bare alias
opusmatched neither field, returnednull, and fell through to the baked-inMODEL_CATALOG— which still pinnedopustoclaude-opus-4-8. A user picking "opus" from a picker that displayedopus[1m]silently got the older model.The live-list-wins design was already right; the alias just never reached it. Now the value is also compared with its bracketed variant suffix stripped, so
opus→opus[1m]. Exact matches are checked across the whole list before any stripped match, so a backend offering bothopusandopus[1m]still resolves the exact entry. The catalog's premium alias moves toclaude-opus-5so the pre-first-report fallback isn't stale either.The existing test passed for the wrong reason
models.test.tsalready assertedresolveAgainstList("opus", live) === "opus[1m]". It passed — because the fixture claimed the entry'sdisplayNamewas"Opus", which made it resolve by a display-name match that does not exist in the real payload. The fixture is now verbatim fromsupportedModels()on 0.3.258. That fiction is why this bug survived a test that looked like it covered it.2. The pinned SDK could not run Fable 5.1 at all
claude-fable-5-1is_error=false,model=claude-fable-5-1The silent Haiku fallback is the sharp edge: a wrong-model completion with no error surfaced to the caller.
The SDK version tracks the bundled CLI 1:1, so the dependency bump is the CLI bump. The manifest floor moves to
^0.3.258with it — 0.3.220 cannot serve the model, which makes it a requirement rather than a preference.Verification
Against the live backend, on the upgraded dependency:
bun test— 2420 pass, 19 skip, 0 fail (161 files)bun run typecheck— cleanbun run lint— cleanNew coverage:
stripVariantSuffix; bare-alias-to-variant matching; exact-beats-stripped precedence; bare-full-id to variant entry;claude-*passthrough for an unadvertised id; and aMODEL_CATALOGassertion that the premium alias maps to the current Opus generation, so the next time it goes stale a test fails instead of a user silently getting an old model.🤖 Generated with Claude Code