Preserve Opus fallback on older Claude Code#4480
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
This is a focused follow-up to #4472, not another Opus 5 integration. It resolves the unresolved review finding at #4472 (comment): after The fix keeps the bare Scope is |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 29a898e. Configure here.
| // cannot change which fallback is selected. | ||
| if (provider === ProviderDriverKind.make("claudeAgent") && trimmed === "opus") { | ||
| const availableSlugs = new Set(options.map((option) => option.slug)); | ||
| return CLAUDE_OPUS_FALLBACK_MODELS.find((slug) => availableSlugs.has(slug)) ?? null; |
There was a problem hiding this comment.
Fallback picks custom Opus slugs
Medium Severity
For bare opus, the new fallback treats any matching option slug in CLAUDE_OPUS_FALLBACK_MODELS as eligible, including custom models added via getAppModelOptionsForInstance. That can resolve opus to a user-defined claude-opus-* custom slug when no built-in Opus is in the catalog, instead of ignoring those slugs as described for this change.
Reviewed by Cursor Bugbot for commit 29a898e. Configure here.
There was a problem hiding this comment.
Thanks — the mechanism is real, but the trigger is narrower than the description suggests, so I have left the behavior as-is for now.
The fallback compares slug strings, so it cannot distinguish a built-in entry from a custom one. I confirmed with a throwaway test that a catalog containing no built-in Opus plus a custom model whose slug is literally claude-opus-4-8 does resolve opus to that custom slug.
Two things bound it:
normalizeCustomModelSlugs()inapps/web/src/modelSelection.tsalready drops any custom slug that collides with a built-in model, so this requires a catalog where the colliding name is not currently built-in (an older Claude Code, on a custom instance, with a deliberately colliding name).- Only the five canonical slugs in
CLAUDE_OPUS_FALLBACK_MODELSare eligible. Arbitrary custom names such asclaude-opus-customare ignored, which is the case covered by the new test.
In that narrow scenario the result is a model the user explicitly named after a real Opus release, which seems closer to intent than falling through to the provider default — which on 2.1.169 and above is claude-fable-5, a different model family. An exact custom model named opus is also still preserved by the direct-slug match before any alias handling.
Fair hit on the PR wording though: "ignore custom claude-opus-* slugs" was looser than the code. I have corrected the description and the summary comment.
If maintainers would rather have this enforced strictly, the fix is to thread the existing isCustom flag into SelectableModelOption and skip custom entries in the fallback. That touches every call site that builds model options, so I kept it out of this focused change — happy to add it here or as a follow-up if you prefer.
ApprovabilityVerdict: Needs human review This PR changes runtime model selection behavior for the 'opus' alias fallback logic, and has an unresolved review comment identifying a potential bug where custom model slugs could incorrectly be selected as fallbacks. You can customize Macroscope's approvability policy. Learn more. |
|
One finding unrelated to this PR, flagged here rather than in the description. Live verification details now live in the description itself. The five tests in Happy to open a separate issue or PR for that if useful; it is out of scope here. |


What Changed
The bare
opusalias now resolves to the newest Opus model the installed Claude Code actually supports, instead of falling through to the provider default when Opus 5 is absent from the catalog.Two files in
packages/shared. No SDK bump, no change to the version gate.Why
#4472 repointed
opustoclaude-opus-5. That slug is gated behindMINIMUM_CLAUDE_OPUS_5_VERSION = "2.1.219"ingetBuiltInClaudeModelsForVersion(), so on older clients it never reaches the served catalog.resolveSelectableModel()returnsnull, and selection falls through togetDefaultServerModel()— which takes the first non-custom model surviving version filtering,claude-fable-5from 2.1.169 up.In practice: on Claude Code 2.1.169–2.1.218, picking
opusgives you Claude Fable 5 while Opus 4.8 is sitting available in the same catalog.Only the second row moves. Below 2.1.169 the default already lands on the newest available Opus, so the fallback is a no-op there.
Falling through to the newest supported model is the right behaviour when nothing was asked for.
opusis a family request though, so it now resolves inside the Opus family first. The default policy itself is untouched.The fallback walks a fixed list of built-in slugs, so
modelOrdercannot shift which Opus is picked andclaude-opus-customis never selected. It reads the visible option list, so a model hidden viahiddenModelsstays unselected. Explicitopus-5still resolves to nothing when Opus 5 is absent, rather than quietly downgrading to a different model.Resolves the open finding on #4472: #4472 (comment)
apps/web/src/modelSelection.test.tspins this end to end: withclaude-fable-5present andclaude-opus-5filtered out,resolveAppModelSelectionreturnsclaude-opus-4-8. Revertingpackages/shared/src/model.tsto 41a430a fails that case withclaude-fable-5.Tests:
vp test run packages/shared/src/model.test.ts apps/web/src/modelSelection.test.ts— 17 passed. Typecheck on@t3tools/sharedand@t3tools/web, targeted lint and format, andgit diff --check— all clean. Claude adapter suite — 62 passed.Verified live on Claude Code 2.1.219 (Windows):
claude -p --model claude-opus-5responds across all five efforts,claude-opus-5[1m]is accepted, and the bareopusalias resolves.Checklist
Note
Preserve Opus model fallback in
resolveSelectableModelfor older Claude CodeWhen the provider is
claudeAgentand the requested value is'opus',resolveSelectableModelnow iterates a fixed priority list (CLAUDE_OPUS_FALLBACK_MODELS) and returns the newest available Claude Opus model slug instead of returning null. This ensures older Claude Code clients that send the bare'opus'alias still resolve to a valid model when the normalized target slug is not present in the available options.Macroscope summarized 29a898e.
Note
Low Risk
Scoped to Claude
opusalias resolution when the normalized slug is missing; no auth, persistence, or version-gate changes.Overview
Fixes a regression where the bare
opusalias could resolve to an unrelated default (e.g. Fable 5) when Opus 5 is missing from the live catalog on Claude Code 2.1.169–2.1.218.resolveSelectableModelin@t3tools/sharednow, forclaudeAgentand inputopus, walks a fixedCLAUDE_OPUS_FALLBACK_MODELSpriority list and picks the first slug present in the caller’s option list. Direct normalization still wins when Opus 5 is available; explicitopus-5still returns nothing if that slug isn’t offered. Custom Opus slugs are not used as fallbacks.Tests cover shared resolution behavior and
resolveAppModelSelectionend-to-end when the served catalog omits Opus 5.Reviewed by Cursor Bugbot for commit de4da06. Bugbot is set up for automated code reviews on this repo. Configure here.