From 29a898ecb1efdcd14905ccc7e6e61bd9bba92649 Mon Sep 17 00:00:00 2001 From: mihneanob Date: Sat, 25 Jul 2026 00:04:31 +0300 Subject: [PATCH 1/2] Preserve Opus fallback on older Claude Code --- packages/shared/src/model.test.ts | 21 +++++++++++++++++++++ packages/shared/src/model.ts | 25 ++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/model.test.ts b/packages/shared/src/model.test.ts index b67c4574473..642019ea89d 100644 --- a/packages/shared/src/model.test.ts +++ b/packages/shared/src/model.test.ts @@ -12,6 +12,7 @@ import { getProviderOptionStringSelectionValue, normalizeCustomModelSlug, normalizeModelSlug, + resolveSelectableModel, } from "./model.ts"; const codexCaps: ModelCapabilities = createModelCapabilities({ @@ -154,4 +155,24 @@ describe("model slug normalization", () => { expect(normalizeModelSlug("opus", claude)).toBe("claude-opus-5"); expect(normalizeCustomModelSlug(" opus ")).toBe("opus"); }); + + it("resolves the bare Opus alias to the newest available Claude model", () => { + const claude = ProviderDriverKind.make("claudeAgent"); + const currentModels = [ + { slug: "claude-opus-5", name: "Claude Opus 5" }, + { slug: "claude-opus-4-8", name: "Claude Opus 4.8" }, + ]; + const olderModels = [ + { slug: "claude-opus-custom", name: "Custom Opus" }, + { slug: "claude-opus-4-6", name: "Claude Opus 4.6" }, + { slug: "claude-sonnet-5", name: "Claude Sonnet 5" }, + { slug: "claude-opus-4-8", name: "Claude Opus 4.8" }, + ]; + const customModels = [{ slug: "claude-opus-custom", name: "Custom Opus" }]; + + expect(resolveSelectableModel(claude, "opus", currentModels)).toBe("claude-opus-5"); + expect(resolveSelectableModel(claude, "opus", olderModels)).toBe("claude-opus-4-8"); + expect(resolveSelectableModel(claude, "opus-5", olderModels)).toBeNull(); + expect(resolveSelectableModel(claude, "opus", customModels)).toBeNull(); + }); }); diff --git a/packages/shared/src/model.ts b/packages/shared/src/model.ts index bdc0c0cc8ef..6ac7b39a240 100644 --- a/packages/shared/src/model.ts +++ b/packages/shared/src/model.ts @@ -12,6 +12,14 @@ import { const DEFAULT_PROVIDER_DRIVER_KIND = ProviderDriverKind.make("codex"); +const CLAUDE_OPUS_FALLBACK_MODELS = [ + "claude-opus-5", + "claude-opus-4-8", + "claude-opus-4-7", + "claude-opus-4-6", + "claude-opus-4-5", +] as const; + export interface SelectableModelOption { slug: string; name: string; @@ -287,7 +295,22 @@ export function resolveSelectableModel( } const resolved = options.find((option) => option.slug === normalized); - return resolved ? resolved.slug : null; + if (resolved) { + return resolved.slug; + } + + // The bare Claude `opus` alias tracks the newest Opus model, but newer + // models can be omitted from the live catalog when the installed Claude + // Code version is too old. In that case, preserve the model family instead + // of falling through to the provider's unrelated default model. Use a fixed + // built-in priority so user-defined picker ordering and custom model slugs + // 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; + } + + return null; } function resolveModelSlug(model: string | null | undefined, provider: ProviderDriverKind): string { From de4da06b92b3165cbcc6a748fb8de07fd9715541 Mon Sep 17 00:00:00 2001 From: mihneanob Date: Sat, 25 Jul 2026 11:18:09 +0300 Subject: [PATCH 2/2] Add regression coverage for the bare Opus fallback --- apps/web/src/modelSelection.test.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/apps/web/src/modelSelection.test.ts b/apps/web/src/modelSelection.test.ts index e8fc8a244d0..f47e7944691 100644 --- a/apps/web/src/modelSelection.test.ts +++ b/apps/web/src/modelSelection.test.ts @@ -4,6 +4,7 @@ import { describe, expect, it } from "vite-plus/test"; import { deriveProviderInstanceEntries } from "./providerInstances"; import { getAppModelOptionsForInstance, + resolveAppModelSelection, resolveAppModelSelectionForInstance, resolveAppModelSelectionState, } from "./modelSelection"; @@ -303,3 +304,28 @@ describe("instance-scoped model selection", () => { }); }); }); + +describe("provider-scoped model selection", () => { + it("keeps the bare Opus alias in the Opus family when Opus 5 is gated out", () => { + // Mirrors Claude Code 2.1.169-2.1.218, where Opus 5 is filtered out of the + // served catalog. The provider default is the first built-in that survives + // filtering (Fable 5), so without the family fallback `opus` resolves to a + // different model family while Opus 4.8 is still available. + const providers = [ + provider({ + provider: ProviderDriverKind.make("claudeAgent"), + instanceId: "claudeAgent", + models: ["claude-fable-5", "claude-opus-4-8", "claude-sonnet-5"], + }), + ]; + + expect( + resolveAppModelSelection( + ProviderDriverKind.make("claudeAgent"), + settingsWithProviderInstances(), + providers, + "opus", + ), + ).toBe("claude-opus-4-8"); + }); +});