From 648c30206749969438d4b47ff213d53ee5044170 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 9 Jun 2026 10:39:18 -0700 Subject: [PATCH 1/2] Add Claude Fable 5 model (#3009) Co-authored-by: codex --- .../src/provider/Layers/ClaudeAdapter.test.ts | 23 +++ .../src/provider/Layers/ClaudeProvider.ts | 146 ++++++++++++------ .../provider/Layers/ProviderRegistry.test.ts | 56 +++++++ 3 files changed, 175 insertions(+), 50 deletions(-) diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts index 0945a4da55e..b7c2b40e5dc 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts @@ -509,6 +509,29 @@ describe("ClaudeAdapterLive", () => { ); }); + it.effect("preserves xhigh effort for Claude Fable 5", () => { + const harness = makeHarness(); + return Effect.gen(function* () { + const adapter = yield* ClaudeAdapter; + yield* adapter.startSession({ + threadId: THREAD_ID, + provider: ProviderDriverKind.make("claudeAgent"), + modelSelection: createModelSelection( + ProviderInstanceId.make("claudeAgent"), + "claude-fable-5", + [{ id: "effort", value: "xhigh" }], + ), + runtimeMode: "full-access", + }); + + const createInput = harness.getLastCreateQueryInput(); + assert.equal(createInput?.options.effort, "xhigh"); + }).pipe( + Effect.provideService(Random.Random, makeDeterministicRandomService()), + Effect.provide(harness.layer), + ); + }); + it.effect("falls back to default effort when unsupported max is requested for Sonnet 4.6", () => { const harness = makeHarness(); return Effect.gen(function* () { diff --git a/apps/server/src/provider/Layers/ClaudeProvider.ts b/apps/server/src/provider/Layers/ClaudeProvider.ts index 26d69686575..b2f22dbcd40 100644 --- a/apps/server/src/provider/Layers/ClaudeProvider.ts +++ b/apps/server/src/provider/Layers/ClaudeProvider.ts @@ -48,50 +48,42 @@ const CLAUDE_PRESENTATION = { displayName: "Claude", showInteractionModeToggle: true, } as const; +const MINIMUM_CLAUDE_FABLE_5_VERSION = "2.1.169"; const MINIMUM_CLAUDE_OPUS_4_8_VERSION = "2.1.154"; const MINIMUM_CLAUDE_OPUS_4_7_VERSION = "2.1.111"; -const CLAUDE_EFFORT_OPTIONS = { - opus48: [ - { value: "low", label: "Low" }, - { value: "medium", label: "Medium" }, - { value: "high", label: "High", isDefault: true }, - { value: "xhigh", label: "Extra High" }, - { value: "max", label: "Max" }, - { value: "ultracode", label: "Ultracode" }, - { value: "ultrathink", label: "Ultrathink" }, - ], - opus47: [ - { value: "low", label: "Low" }, - { value: "medium", label: "Medium" }, - { value: "high", label: "High" }, - { value: "xhigh", label: "Extra High", isDefault: true }, - { value: "max", label: "Max" }, - { value: "ultrathink", label: "Ultrathink" }, - ], - opus46: [ - { value: "low", label: "Low" }, - { value: "medium", label: "Medium" }, - { value: "high", label: "High", isDefault: true }, - { value: "max", label: "Max" }, - { value: "ultrathink", label: "Ultrathink" }, - ], - sonnet46: [ - { value: "low", label: "Low" }, - { value: "medium", label: "Medium" }, - { value: "high", label: "High", isDefault: true }, - { value: "max", label: "Max" }, - { value: "ultrathink", label: "Ultrathink" }, - ], - opus45: [ - { value: "low", label: "Low" }, - { value: "medium", label: "Medium" }, - { value: "high", label: "High", isDefault: true }, - { value: "max", label: "Max" }, - ], -} as const; - const BUILT_IN_MODELS: ReadonlyArray = [ + { + slug: "claude-fable-5", + name: "Claude Fable 5", + isCustom: false, + capabilities: createModelCapabilities({ + optionDescriptors: [ + buildSelectOptionDescriptor({ + id: "effort", + label: "Reasoning", + options: [ + { value: "low", label: "Low" }, + { value: "medium", label: "Medium" }, + { value: "high", label: "High", isDefault: true }, + { value: "xhigh", label: "Extra High" }, + { value: "max", label: "Max" }, + { value: "ultracode", label: "Ultracode" }, + { value: "ultrathink", label: "Ultrathink" }, + ], + promptInjectedValues: ["ultrathink"], + }), + buildSelectOptionDescriptor({ + id: "contextWindow", + label: "Context Window", + options: [ + { value: "200k", label: "200k", isDefault: true }, + { value: "1m", label: "1M" }, + ], + }), + ], + }), + }, { slug: "claude-opus-4-8", name: "Claude Opus 4.8", @@ -101,9 +93,21 @@ const BUILT_IN_MODELS: ReadonlyArray = [ buildSelectOptionDescriptor({ id: "effort", label: "Reasoning", - options: CLAUDE_EFFORT_OPTIONS.opus48, + options: [ + { value: "low", label: "Low" }, + { value: "medium", label: "Medium" }, + { value: "high", label: "High", isDefault: true }, + { value: "xhigh", label: "Extra High" }, + { value: "max", label: "Max" }, + { value: "ultracode", label: "Ultracode" }, + { value: "ultrathink", label: "Ultrathink" }, + ], promptInjectedValues: ["ultrathink"], }), + buildBooleanOptionDescriptor({ + id: "fastMode", + label: "Fast Mode", + }), buildSelectOptionDescriptor({ id: "contextWindow", label: "Context Window", @@ -124,9 +128,20 @@ const BUILT_IN_MODELS: ReadonlyArray = [ buildSelectOptionDescriptor({ id: "effort", label: "Reasoning", - options: CLAUDE_EFFORT_OPTIONS.opus47, + options: [ + { value: "low", label: "Low" }, + { value: "medium", label: "Medium" }, + { value: "high", label: "High" }, + { value: "xhigh", label: "Extra High", isDefault: true }, + { value: "max", label: "Max" }, + { value: "ultrathink", label: "Ultrathink" }, + ], promptInjectedValues: ["ultrathink"], }), + buildBooleanOptionDescriptor({ + id: "fastMode", + label: "Fast Mode", + }), buildSelectOptionDescriptor({ id: "contextWindow", label: "Context Window", @@ -147,7 +162,13 @@ const BUILT_IN_MODELS: ReadonlyArray = [ buildSelectOptionDescriptor({ id: "effort", label: "Reasoning", - options: CLAUDE_EFFORT_OPTIONS.opus46, + options: [ + { value: "low", label: "Low" }, + { value: "medium", label: "Medium" }, + { value: "high", label: "High", isDefault: true }, + { value: "max", label: "Max" }, + { value: "ultrathink", label: "Ultrathink" }, + ], promptInjectedValues: ["ultrathink"], }), buildBooleanOptionDescriptor({ @@ -174,7 +195,12 @@ const BUILT_IN_MODELS: ReadonlyArray = [ buildSelectOptionDescriptor({ id: "effort", label: "Reasoning", - options: CLAUDE_EFFORT_OPTIONS.opus45, + options: [ + { value: "low", label: "Low" }, + { value: "medium", label: "Medium" }, + { value: "high", label: "High", isDefault: true }, + { value: "max", label: "Max" }, + ], }), buildBooleanOptionDescriptor({ id: "fastMode", @@ -192,7 +218,13 @@ const BUILT_IN_MODELS: ReadonlyArray = [ buildSelectOptionDescriptor({ id: "effort", label: "Reasoning", - options: CLAUDE_EFFORT_OPTIONS.sonnet46, + options: [ + { value: "low", label: "Low" }, + { value: "medium", label: "Medium" }, + { value: "high", label: "High", isDefault: true }, + { value: "max", label: "Max" }, + { value: "ultrathink", label: "Ultrathink" }, + ], promptInjectedValues: ["ultrathink"], }), buildSelectOptionDescriptor({ @@ -221,6 +253,10 @@ const BUILT_IN_MODELS: ReadonlyArray = [ }, ]; +function supportsClaudeFable5(version: string | null | undefined): boolean { + return version ? compareSemverVersions(version, MINIMUM_CLAUDE_FABLE_5_VERSION) >= 0 : false; +} + function supportsClaudeOpus48(version: string | null | undefined): boolean { return version ? compareSemverVersions(version, MINIMUM_CLAUDE_OPUS_4_8_VERSION) >= 0 : false; } @@ -233,6 +269,9 @@ function getBuiltInClaudeModelsForVersion( version: string | null | undefined, ): ReadonlyArray { return BUILT_IN_MODELS.filter((model) => { + if (model.slug === "claude-fable-5") { + return supportsClaudeFable5(version); + } if (model.slug === "claude-opus-4-8") { return supportsClaudeOpus48(version); } @@ -243,6 +282,11 @@ function getBuiltInClaudeModelsForVersion( }); } +function formatClaudeFable5UpgradeMessage(version: string | null): string { + const versionLabel = version ? `v${version}` : "the installed version"; + return `Claude Code ${versionLabel} is too old for Claude Fable 5. Upgrade to v${MINIMUM_CLAUDE_FABLE_5_VERSION} or newer to access it.`; +} + function formatClaudeOpus48UpgradeMessage(version: string | null): string { const versionLabel = version ? `v${version}` : "the installed version"; return `Claude Code ${versionLabel} is too old for Claude Opus 4.8. Upgrade to v${MINIMUM_CLAUDE_OPUS_4_8_VERSION} or newer to access it.`; @@ -294,7 +338,7 @@ export function normalizeClaudeCliEffort( if (effort === "ultracode") { return "xhigh"; } - if (effort === "xhigh" && model !== "claude-opus-4-8") { + if (effort === "xhigh" && model !== "claude-fable-5" && model !== "claude-opus-4-8") { return "max"; } if (effort === "max" && model === "claude-sonnet-4-6") { @@ -688,11 +732,13 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( claudeSettings.customModels, DEFAULT_CLAUDE_MODEL_CAPABILITIES, ); - const versionUpgradeMessage = supportsClaudeOpus48(parsedVersion) + const versionUpgradeMessage = supportsClaudeFable5(parsedVersion) ? undefined - : supportsClaudeOpus47(parsedVersion) - ? formatClaudeOpus48UpgradeMessage(parsedVersion) - : formatClaudeOpus47UpgradeMessage(parsedVersion); + : supportsClaudeOpus48(parsedVersion) + ? formatClaudeFable5UpgradeMessage(parsedVersion) + : supportsClaudeOpus47(parsedVersion) + ? formatClaudeOpus48UpgradeMessage(parsedVersion) + : formatClaudeOpus47UpgradeMessage(parsedVersion); const capabilities = resolveCapabilities ? yield* resolveCapabilities(claudeSettings).pipe(Effect.orElseSucceed(() => undefined)) diff --git a/apps/server/src/provider/Layers/ProviderRegistry.test.ts b/apps/server/src/provider/Layers/ProviderRegistry.test.ts index cca1e12e48b..772fdfc1565 100644 --- a/apps/server/src/provider/Layers/ProviderRegistry.test.ts +++ b/apps/server/src/provider/Layers/ProviderRegistry.test.ts @@ -1379,6 +1379,62 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T ), ); + it.effect("includes Claude Fable 5 on supported Claude Code versions", () => + Effect.gen(function* () { + const status = yield* checkClaudeProviderStatus( + defaultClaudeSettings, + claudeCapabilities(), + ); + const fable5 = status.models.find((model) => model.slug === "claude-fable-5"); + assert.strictEqual(fable5?.name, "Claude Fable 5"); + }).pipe( + Effect.provide( + mockSpawnerLayer((args) => { + const joined = args.join(" "); + if (joined === "--version") return { stdout: "2.1.169\n", stderr: "", code: 0 }; + if (joined === "auth status") + return { + stdout: '{"loggedIn":true,"authMethod":"claude.ai"}\n', + stderr: "", + code: 0, + }; + throw new Error(`Unexpected args: ${joined}`); + }), + ), + ), + ); + + it.effect("hides Claude Fable 5 on older Claude Code versions", () => + Effect.gen(function* () { + const status = yield* checkClaudeProviderStatus( + defaultClaudeSettings, + claudeCapabilities(), + ); + assert.strictEqual( + status.models.some((model) => model.slug === "claude-fable-5"), + false, + ); + assert.strictEqual( + status.message, + "Claude Code v2.1.168 is too old for Claude Fable 5. Upgrade to v2.1.169 or newer to access it.", + ); + }).pipe( + Effect.provide( + mockSpawnerLayer((args) => { + const joined = args.join(" "); + if (joined === "--version") return { stdout: "2.1.168\n", stderr: "", code: 0 }; + if (joined === "auth status") + return { + stdout: '{"loggedIn":true,"authMethod":"claude.ai"}\n', + stderr: "", + code: 0, + }; + throw new Error(`Unexpected args: ${joined}`); + }), + ), + ), + ); + it.effect( "includes Claude Opus 4.7 with xhigh as the default effort on supported versions", () => From 31a6a3156a198df8f904cc3acec9c8b2cfec8255 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 11:40:35 -0700 Subject: [PATCH 2/2] Add Claude Sonnet 5 as the default Claude model (#3620) --- .../src/provider/Layers/ClaudeProvider.ts | 37 ++++++++++++++++++- .../settings/ProviderModelsSection.tsx | 2 +- packages/contracts/src/model.ts | 7 +++- packages/shared/src/model.test.ts | 6 ++- 4 files changed, 46 insertions(+), 6 deletions(-) diff --git a/apps/server/src/provider/Layers/ClaudeProvider.ts b/apps/server/src/provider/Layers/ClaudeProvider.ts index b2f22dbcd40..1f620c1363c 100644 --- a/apps/server/src/provider/Layers/ClaudeProvider.ts +++ b/apps/server/src/provider/Layers/ClaudeProvider.ts @@ -209,6 +209,36 @@ const BUILT_IN_MODELS: ReadonlyArray = [ ], }), }, + { + slug: "claude-sonnet-5", + name: "Claude Sonnet 5", + isCustom: false, + capabilities: createModelCapabilities({ + optionDescriptors: [ + buildSelectOptionDescriptor({ + id: "effort", + label: "Reasoning", + options: [ + { value: "low", label: "Low" }, + { value: "medium", label: "Medium" }, + { value: "high", label: "High", isDefault: true }, + { value: "xhigh", label: "Extra High" }, + { value: "max", label: "Max" }, + { value: "ultrathink", label: "Ultrathink" }, + ], + promptInjectedValues: ["ultrathink"], + }), + buildSelectOptionDescriptor({ + id: "contextWindow", + label: "Context Window", + options: [ + { value: "200k", label: "200k", isDefault: true }, + { value: "1m", label: "1M" }, + ], + }), + ], + }), + }, { slug: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", @@ -338,7 +368,12 @@ export function normalizeClaudeCliEffort( if (effort === "ultracode") { return "xhigh"; } - if (effort === "xhigh" && model !== "claude-fable-5" && model !== "claude-opus-4-8") { + if ( + effort === "xhigh" && + model !== "claude-fable-5" && + model !== "claude-opus-4-8" && + model !== "claude-sonnet-5" + ) { return "max"; } if (effort === "max" && model === "claude-sonnet-4-6") { diff --git a/apps/web/src/components/settings/ProviderModelsSection.tsx b/apps/web/src/components/settings/ProviderModelsSection.tsx index 5db713495b9..f1100ab93d2 100644 --- a/apps/web/src/components/settings/ProviderModelsSection.tsx +++ b/apps/web/src/components/settings/ProviderModelsSection.tsx @@ -32,7 +32,7 @@ import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; */ const CUSTOM_MODEL_PLACEHOLDER_BY_KIND: Partial> = { [ProviderDriverKind.make("codex")]: "gpt-6.7-codex-ultra-preview", - [ProviderDriverKind.make("claudeAgent")]: "claude-sonnet-5-0", + [ProviderDriverKind.make("claudeAgent")]: "claude-sonnet-5", [ProviderDriverKind.make("cursor")]: "claude-sonnet-4-6", [ProviderDriverKind.make("opencode")]: "openai/gpt-5", }; diff --git a/packages/contracts/src/model.ts b/packages/contracts/src/model.ts index 3fdb0d58c7f..562d196b24f 100644 --- a/packages/contracts/src/model.ts +++ b/packages/contracts/src/model.ts @@ -137,7 +137,7 @@ export const DEFAULT_GIT_TEXT_GENERATION_MODEL = "gpt-5.4-mini"; export const DEFAULT_MODEL_BY_PROVIDER: Partial> = { [CODEX_DRIVER_KIND]: DEFAULT_MODEL, - [CLAUDE_DRIVER_KIND]: "claude-sonnet-4-6", + [CLAUDE_DRIVER_KIND]: "claude-sonnet-5", [CURSOR_DRIVER_KIND]: "auto", [OPENCODE_DRIVER_KIND]: "openai/gpt-5", }; @@ -175,7 +175,10 @@ export const MODEL_SLUG_ALIASES_BY_PROVIDER: Partial< "opus-4.6": "claude-opus-4-6", "claude-opus-4.6": "claude-opus-4-6", "claude-opus-4-6-20251117": "claude-opus-4-6", - sonnet: "claude-sonnet-4-6", + sonnet: "claude-sonnet-5", + "sonnet-5": "claude-sonnet-5", + "claude-sonnet-5.0": "claude-sonnet-5", + "claude-sonnet-5-0": "claude-sonnet-5", "sonnet-4.6": "claude-sonnet-4-6", "claude-sonnet-4.6": "claude-sonnet-4-6", "claude-sonnet-4-6-20251117": "claude-sonnet-4-6", diff --git a/packages/shared/src/model.test.ts b/packages/shared/src/model.test.ts index 61cc8d181f4..ea5a52707d7 100644 --- a/packages/shared/src/model.test.ts +++ b/packages/shared/src/model.test.ts @@ -75,7 +75,8 @@ describe("normalizeModelSlug", () => { const claude = ProviderDriverKind.make("claudeAgent"); expect(normalizeModelSlug("gpt-5-codex")).toBe("gpt-5.4"); expect(normalizeModelSlug("5.3")).toBe("gpt-5.3-codex"); - expect(normalizeModelSlug("sonnet", claude)).toBe("claude-sonnet-4-6"); + expect(normalizeModelSlug("sonnet", claude)).toBe("claude-sonnet-5"); + expect(normalizeModelSlug("sonnet-4.6", claude)).toBe("claude-sonnet-4-6"); }); it("returns null for empty or missing values", () => { @@ -108,6 +109,7 @@ describe("resolveSelectableModel", () => { const options = [ { slug: "gpt-5.3-codex", name: "GPT-5.3 Codex" }, { slug: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" }, + { slug: "claude-sonnet-5", name: "Claude Sonnet 5" }, ]; expect(resolveSelectableModel(ProviderDriverKind.make("codex"), "gpt-5.3-codex", options)).toBe( "gpt-5.3-codex", @@ -116,7 +118,7 @@ describe("resolveSelectableModel", () => { "gpt-5.3-codex", ); expect(resolveSelectableModel(ProviderDriverKind.make("claudeAgent"), "sonnet", options)).toBe( - "claude-sonnet-4-6", + "claude-sonnet-5", ); }); });