From c8cbc045d365c115027ce8f60592d906cd59b343 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 9 Jun 2026 10:21:29 -0700 Subject: [PATCH 1/6] feat: add Claude Fable 5 model Co-authored-by: codex --- apps/server/src/provider/Layers/ClaudeProvider.ts | 6 ++++++ apps/server/src/provider/Layers/ProviderRegistry.test.ts | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/apps/server/src/provider/Layers/ClaudeProvider.ts b/apps/server/src/provider/Layers/ClaudeProvider.ts index badbf98905a..debf4513eb9 100644 --- a/apps/server/src/provider/Layers/ClaudeProvider.ts +++ b/apps/server/src/provider/Layers/ClaudeProvider.ts @@ -206,6 +206,12 @@ const BUILT_IN_MODELS: ReadonlyArray = [ ], }), }, + { + slug: "claude-fable-5", + name: "Claude Fable 5", + isCustom: false, + capabilities: DEFAULT_CLAUDE_MODEL_CAPABILITIES, + }, { slug: "claude-haiku-4-5", name: "Claude Haiku 4.5", diff --git a/apps/server/src/provider/Layers/ProviderRegistry.test.ts b/apps/server/src/provider/Layers/ProviderRegistry.test.ts index 8621ec06b50..1a46fb8c650 100644 --- a/apps/server/src/provider/Layers/ProviderRegistry.test.ts +++ b/apps/server/src/provider/Layers/ProviderRegistry.test.ts @@ -1350,6 +1350,15 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T assert.strictEqual(status.status, "ready"); assert.strictEqual(status.installed, true); assert.strictEqual(status.auth.status, "authenticated"); + assert.deepStrictEqual( + status.models.find((model) => model.slug === "claude-fable-5"), + { + slug: "claude-fable-5", + name: "Claude Fable 5", + isCustom: false, + capabilities: { optionDescriptors: [] }, + }, + ); }).pipe( Effect.provide( mockSpawnerLayer((args) => { From 22af73b1bf490e47734306921814a16d0f6e1c2f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 9 Jun 2026 10:26:13 -0700 Subject: [PATCH 2/6] Add Claude Fable 5 provider capabilities - Define Fable 5 reasoning and context window options - Add fast mode to Opus 4.7 and 4.8 models --- .../src/provider/Layers/ClaudeProvider.ts | 46 ++++++++++++++++--- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/apps/server/src/provider/Layers/ClaudeProvider.ts b/apps/server/src/provider/Layers/ClaudeProvider.ts index debf4513eb9..7eed8dc720b 100644 --- a/apps/server/src/provider/Layers/ClaudeProvider.ts +++ b/apps/server/src/provider/Layers/ClaudeProvider.ts @@ -52,6 +52,15 @@ const MINIMUM_CLAUDE_OPUS_4_8_VERSION = "2.1.154"; const MINIMUM_CLAUDE_OPUS_4_7_VERSION = "2.1.111"; const CLAUDE_EFFORT_OPTIONS = { + fable48: [ + { 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" }, + ], opus48: [ { value: "low", label: "Low" }, { value: "medium", label: "Medium" }, @@ -92,6 +101,29 @@ const CLAUDE_EFFORT_OPTIONS = { } 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: CLAUDE_EFFORT_OPTIONS.fable48, + 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", @@ -104,6 +136,10 @@ const BUILT_IN_MODELS: ReadonlyArray = [ options: CLAUDE_EFFORT_OPTIONS.opus48, promptInjectedValues: ["ultrathink"], }), + buildBooleanOptionDescriptor({ + id: "fastMode", + label: "Fast Mode", + }), buildSelectOptionDescriptor({ id: "contextWindow", label: "Context Window", @@ -127,6 +163,10 @@ const BUILT_IN_MODELS: ReadonlyArray = [ options: CLAUDE_EFFORT_OPTIONS.opus47, promptInjectedValues: ["ultrathink"], }), + buildBooleanOptionDescriptor({ + id: "fastMode", + label: "Fast Mode", + }), buildSelectOptionDescriptor({ id: "contextWindow", label: "Context Window", @@ -206,12 +246,6 @@ const BUILT_IN_MODELS: ReadonlyArray = [ ], }), }, - { - slug: "claude-fable-5", - name: "Claude Fable 5", - isCustom: false, - capabilities: DEFAULT_CLAUDE_MODEL_CAPABILITIES, - }, { slug: "claude-haiku-4-5", name: "Claude Haiku 4.5", From 648538a6b9025f12a4788a88cafe744a9d4aadfb Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 9 Jun 2026 10:30:30 -0700 Subject: [PATCH 3/6] test: avoid coupling Claude model presence to capabilities Co-authored-by: codex --- .../src/provider/Layers/ProviderRegistry.test.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/apps/server/src/provider/Layers/ProviderRegistry.test.ts b/apps/server/src/provider/Layers/ProviderRegistry.test.ts index 1a46fb8c650..412250cbaee 100644 --- a/apps/server/src/provider/Layers/ProviderRegistry.test.ts +++ b/apps/server/src/provider/Layers/ProviderRegistry.test.ts @@ -1350,15 +1350,8 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T assert.strictEqual(status.status, "ready"); assert.strictEqual(status.installed, true); assert.strictEqual(status.auth.status, "authenticated"); - assert.deepStrictEqual( - status.models.find((model) => model.slug === "claude-fable-5"), - { - slug: "claude-fable-5", - name: "Claude Fable 5", - isCustom: false, - capabilities: { optionDescriptors: [] }, - }, - ); + const fable5 = status.models.find((model) => model.slug === "claude-fable-5"); + assert.strictEqual(fable5?.name, "Claude Fable 5"); }).pipe( Effect.provide( mockSpawnerLayer((args) => { From e8d43cb7166e966228b09117847adf1a739564ca Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 9 Jun 2026 10:32:59 -0700 Subject: [PATCH 4/6] feat: gate Claude Fable 5 by CLI version Co-authored-by: codex --- .../src/provider/Layers/ClaudeProvider.ts | 23 ++++++-- .../provider/Layers/ProviderRegistry.test.ts | 56 ++++++++++++++++++- 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/apps/server/src/provider/Layers/ClaudeProvider.ts b/apps/server/src/provider/Layers/ClaudeProvider.ts index 7eed8dc720b..d47e6d0d237 100644 --- a/apps/server/src/provider/Layers/ClaudeProvider.ts +++ b/apps/server/src/provider/Layers/ClaudeProvider.ts @@ -48,6 +48,7 @@ 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"; @@ -261,6 +262,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; } @@ -273,6 +278,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); } @@ -283,6 +291,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.`; @@ -728,11 +741,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 412250cbaee..273cf22f31a 100644 --- a/apps/server/src/provider/Layers/ProviderRegistry.test.ts +++ b/apps/server/src/provider/Layers/ProviderRegistry.test.ts @@ -1350,13 +1350,67 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T assert.strictEqual(status.status, "ready"); assert.strictEqual(status.installed, true); assert.strictEqual(status.auth.status, "authenticated"); + }).pipe( + Effect.provide( + mockSpawnerLayer((args) => { + const joined = args.join(" "); + if (joined === "--version") return { stdout: "1.0.0\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 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: "1.0.0\n", stderr: "", code: 0 }; + 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', From 9f57633102c311ebecbe28a78fdf43a17a1a42d1 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 9 Jun 2026 10:35:30 -0700 Subject: [PATCH 5/6] fix: preserve xhigh effort for Claude Fable 5 Co-authored-by: codex --- .../src/provider/Layers/ClaudeAdapter.test.ts | 23 +++++++++++++ .../src/provider/Layers/ClaudeProvider.ts | 33 ++++++++++++++++--- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts index aad1654ecd9..dfaff75b8c1 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts @@ -447,6 +447,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 d47e6d0d237..6db3e7ccc4c 100644 --- a/apps/server/src/provider/Layers/ClaudeProvider.ts +++ b/apps/server/src/provider/Layers/ClaudeProvider.ts @@ -53,7 +53,7 @@ const MINIMUM_CLAUDE_OPUS_4_8_VERSION = "2.1.154"; const MINIMUM_CLAUDE_OPUS_4_7_VERSION = "2.1.111"; const CLAUDE_EFFORT_OPTIONS = { - fable48: [ + fable50: [ { value: "low", label: "Low" }, { value: "medium", label: "Medium" }, { value: "high", label: "High", isDefault: true }, @@ -111,7 +111,15 @@ const BUILT_IN_MODELS: ReadonlyArray = [ buildSelectOptionDescriptor({ id: "effort", label: "Reasoning", - options: CLAUDE_EFFORT_OPTIONS.fable48, + 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({ @@ -134,7 +142,15 @@ 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({ @@ -161,7 +177,14 @@ 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({ @@ -347,7 +370,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") { From cbe8a206ae79ca8f1d5f7b070cc238df236e0804 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 9 Jun 2026 10:36:19 -0700 Subject: [PATCH 6/6] Inline Claude effort options for new models - Remove shared effort presets - Define model-specific reasoning options for Fable 5, Opus 4.6, and Sonnet 4.6 --- .../src/provider/Layers/ClaudeProvider.ts | 72 ++++++------------- 1 file changed, 20 insertions(+), 52 deletions(-) diff --git a/apps/server/src/provider/Layers/ClaudeProvider.ts b/apps/server/src/provider/Layers/ClaudeProvider.ts index 6db3e7ccc4c..3b3eb99c84f 100644 --- a/apps/server/src/provider/Layers/ClaudeProvider.ts +++ b/apps/server/src/provider/Layers/ClaudeProvider.ts @@ -52,55 +52,6 @@ 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 = { - fable50: [ - { 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" }, - ], - 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", @@ -211,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({ @@ -238,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", @@ -256,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({