Skip to content

Preserve Opus fallback on older Claude Code#4480

Open
mihneaptu wants to merge 2 commits into
pingdotgg:mainfrom
mihneaptu:agent/fix-opus5-compat
Open

Preserve Opus fallback on older Claude Code#4480
mihneaptu wants to merge 2 commits into
pingdotgg:mainfrom
mihneaptu:agent/fix-opus5-compat

Conversation

@mihneaptu

@mihneaptu mihneaptu commented Jul 24, 2026

Copy link
Copy Markdown

What Changed

The bare opus alias 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 opus to claude-opus-5. That slug is gated behind MINIMUM_CLAUDE_OPUS_5_VERSION = "2.1.219" in getBuiltInClaudeModelsForVersion(), so on older clients it never reaches the served catalog. resolveSelectableModel() returns null, and selection falls through to getDefaultServerModel() — which takes the first non-custom model surviving version filtering, claude-fable-5 from 2.1.169 up.

In practice: on Claude Code 2.1.169–2.1.218, picking opus gives you Claude Fable 5 while Opus 4.8 is sitting available in the same catalog.

Claude Code before after
>= 2.1.219 Opus 5 Opus 5 — direct match, fallback never runs
2.1.169 – 2.1.218 Fable 5 Opus 4.8
2.1.154 – 2.1.168 Opus 4.8 unchanged
< 2.1.154 / unknown Opus 4.7 / 4.6 unchanged

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. opus is 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 modelOrder cannot shift which Opus is picked and claude-opus-custom is never selected. It reads the visible option list, so a model hidden via hiddenModels stays unselected. Explicit opus-5 still 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.ts pins this end to end: with claude-fable-5 present and claude-opus-5 filtered out, resolveAppModelSelection returns claude-opus-4-8. Reverting packages/shared/src/model.ts to 41a430a fails that case with claude-fable-5.

Tests: vp test run packages/shared/src/model.test.ts apps/web/src/modelSelection.test.ts — 17 passed. Typecheck on @t3tools/shared and @t3tools/web, targeted lint and format, and git diff --check — all clean. Claude adapter suite — 62 passed.

Verified live on Claude Code 2.1.219 (Windows): claude -p --model claude-opus-5 responds across all five efforts, claude-opus-5[1m] is accepted, and the bare opus alias resolves.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes — n/a, no UI changes
  • I included a video for animation/interaction changes — n/a

Note

Preserve Opus model fallback in resolveSelectableModel for older Claude Code

When the provider is claudeAgent and the requested value is 'opus', resolveSelectableModel now 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 opus alias resolution when the normalized slug is missing; no auth, persistence, or version-gate changes.

Overview
Fixes a regression where the bare opus alias 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.

resolveSelectableModel in @t3tools/shared now, for claudeAgent and input opus, walks a fixed CLAUDE_OPUS_FALLBACK_MODELS priority list and picks the first slug present in the caller’s option list. Direct normalization still wins when Opus 5 is available; explicit opus-5 still returns nothing if that slug isn’t offered. Custom Opus slugs are not used as fallbacks.

Tests cover shared resolution behavior and resolveAppModelSelection end-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.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8f2d186e-69db-4a50-b559-c72d6c9059e6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:S 10-29 changed lines (additions + deletions). labels Jul 24, 2026
@mihneaptu
mihneaptu marked this pull request as ready for review July 24, 2026 22:02
@mihneaptu

mihneaptu commented Jul 24, 2026

Copy link
Copy Markdown
Author

This is a focused follow-up to #4472, not another Opus 5 integration.

It resolves the unresolved review finding at #4472 (comment): after opus was repointed to claude-opus-5, Claude Code versions below 2.1.219 omit Opus 5 from the live catalog, so resolveSelectableModel() returned null and selection fell through to the provider default — claude-fable-5 on 2.1.169 and above — instead of the newest available Opus.

The fix keeps the bare opus alias inside the Opus family by walking a fixed built-in priority list, so it is independent of the user's modelOrder preferences and never selects an arbitrary custom name such as claude-opus-custom. Explicit opus-5 still resolves to nothing when Opus 5 is absent, and a custom model named exactly opus is still preserved by the existing direct-match path.

Scope is packages/shared, plus a regression test in apps/web. No SDK bump and no changes to the version gate.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 29a898e. Configure here.

@mihneaptu mihneaptu Jul 24, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. normalizeCustomModelSlugs() in apps/web/src/modelSelection.ts already 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).
  2. Only the five canonical slugs in CLAUDE_OPUS_FALLBACK_MODELS are eligible. Arbitrary custom names such as claude-opus-custom are 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.

@macroscopeapp

macroscopeapp Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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.

@mihneaptu

mihneaptu commented Jul 24, 2026

Copy link
Copy Markdown
Author

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 apps/server/src/textGeneration/ClaudeTextGeneration.test.ts fail on Windows on unmodified main at 41a430a, independently of this change. They are not hermetic on Windows: the fixture prepends a fake claude script to PATH using a POSIX : separator and writes it extension-less, so the Windows resolver skips it (no PATHEXT match) and the real installed CLI is invoked instead. Failures therefore appear as live model prose replacing expected fixtures, or as live 429 / Not logged in errors.

Happy to open a separate issue or PR for that if useful; it is out of scope here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S 10-29 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant