Skip to content

fix: unstick the model alias from a superseded Opus, bump the agent SDK - #315

Merged
saucam merged 1 commit into
mainfrom
fix/model-catalog-and-sdk-bump
Sep 2, 2026
Merged

fix: unstick the model alias from a superseded Opus, bump the agent SDK#315
saucam merged 1 commit into
mainfrom
fix/model-catalog-and-sdk-bump

Conversation

@saucam

@saucam saucam commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Two coupled problems, both invisible until you ask for a model by name.

1. opus resolved to 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 field, returned null, and fell through to the baked-in MODEL_CATALOG — which still pinned opus to claude-opus-4-8. A user picking "opus" from a picker that displayed opus[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 opusopus[1m]. Exact matches are checked across the whole list before any stripped match, 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.

The existing test passed for the wrong reason

models.test.ts already asserted resolveAgainstList("opus", live) === "opus[1m]". It passed — because the 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 this bug survived a test that looked like it covered it.

2. The pinned SDK could not run Fable 5.1 at all

SDK Bundled CLI claude-fable-5-1
0.3.220 (was) Claude Code 2.1.220 400 — "version 2.1.251 or newer is required", then silently completed on Haiku
0.3.258 (now) Claude Code 2.1.258 is_error=false, model=claude-fable-5-1

The 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.258 with 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:

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
  • bun test — 2420 pass, 19 skip, 0 fail (161 files)
  • bun run typecheck — clean
  • bun run lint — clean

New coverage: stripVariantSuffix; bare-alias-to-variant matching; exact-beats-stripped precedence; bare-full-id to variant entry; claude-* passthrough for an unadvertised id; and a MODEL_CATALOG assertion 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

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>
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updated@​anthropic-ai/​claude-agent-sdk@​0.3.220 ⏵ 0.3.258100 +2010092 +110070

View full report

@saucam
saucam merged commit 8bec871 into main Sep 2, 2026
4 checks passed
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants