authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-09 10:39:18-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-09 17:39:18+00:00
logde58ec8e24088711ebe78ffaa73ee5a57a096120
treead3892f1c96e5ce40a0fcb575e5c62e6718a1592
parent8e6f4229da96076ac26e904ce9c5fa678c5def83
signature Signed by PGP key B5690EEEBB952194

Add Claude Fable 5 model (#3009)

Co-authored-by: codex <codex@users.noreply.github.com>

3 files changed, 175 insertions(+), 50 deletions(-)

apps/server/src/provider/Layers/ClaudeAdapter.test.ts+23
...@@ -447,6 +447,29 @@ describe("ClaudeAdapterLive", () => {...@@ -447,6 +447,29 @@ describe("ClaudeAdapterLive", () => {
447 );447 );
448 });448 });
449449
450 it.effect("preserves xhigh effort for Claude Fable 5", () => {
451 const harness = makeHarness();
452 return Effect.gen(function* () {
453 const adapter = yield* ClaudeAdapter;
454 yield* adapter.startSession({
455 threadId: THREAD_ID,
456 provider: ProviderDriverKind.make("claudeAgent"),
457 modelSelection: createModelSelection(
458 ProviderInstanceId.make("claudeAgent"),
459 "claude-fable-5",
460 [{ id: "effort", value: "xhigh" }],
461 ),
462 runtimeMode: "full-access",
463 });
464
465 const createInput = harness.getLastCreateQueryInput();
466 assert.equal(createInput?.options.effort, "xhigh");
467 }).pipe(
468 Effect.provideService(Random.Random, makeDeterministicRandomService()),
469 Effect.provide(harness.layer),
470 );
471 });
472
450 it.effect("falls back to default effort when unsupported max is requested for Sonnet 4.6", () => {473 it.effect("falls back to default effort when unsupported max is requested for Sonnet 4.6", () => {
451 const harness = makeHarness();474 const harness = makeHarness();
452 return Effect.gen(function* () {475 return Effect.gen(function* () {
apps/server/src/provider/Layers/ClaudeProvider.ts+96-50
...@@ -48,50 +48,42 @@ const CLAUDE_PRESENTATION = {...@@ -48,50 +48,42 @@ const CLAUDE_PRESENTATION = {
48 displayName: "Claude",48 displayName: "Claude",
49 showInteractionModeToggle: true,49 showInteractionModeToggle: true,
50} as const;50} as const;
51const MINIMUM_CLAUDE_FABLE_5_VERSION = "2.1.169";
51const MINIMUM_CLAUDE_OPUS_4_8_VERSION = "2.1.154";52const MINIMUM_CLAUDE_OPUS_4_8_VERSION = "2.1.154";
52const MINIMUM_CLAUDE_OPUS_4_7_VERSION = "2.1.111";53const MINIMUM_CLAUDE_OPUS_4_7_VERSION = "2.1.111";
5354
54const CLAUDE_EFFORT_OPTIONS = {
55 opus48: [
56 { value: "low", label: "Low" },
57 { value: "medium", label: "Medium" },
58 { value: "high", label: "High", isDefault: true },
59 { value: "xhigh", label: "Extra High" },
60 { value: "max", label: "Max" },
61 { value: "ultracode", label: "Ultracode" },
62 { value: "ultrathink", label: "Ultrathink" },
63 ],
64 opus47: [
65 { value: "low", label: "Low" },
66 { value: "medium", label: "Medium" },
67 { value: "high", label: "High" },
68 { value: "xhigh", label: "Extra High", isDefault: true },
69 { value: "max", label: "Max" },
70 { value: "ultrathink", label: "Ultrathink" },
71 ],
72 opus46: [
73 { value: "low", label: "Low" },
74 { value: "medium", label: "Medium" },
75 { value: "high", label: "High", isDefault: true },
76 { value: "max", label: "Max" },
77 { value: "ultrathink", label: "Ultrathink" },
78 ],
79 sonnet46: [
80 { value: "low", label: "Low" },
81 { value: "medium", label: "Medium" },
82 { value: "high", label: "High", isDefault: true },
83 { value: "max", label: "Max" },
84 { value: "ultrathink", label: "Ultrathink" },
85 ],
86 opus45: [
87 { value: "low", label: "Low" },
88 { value: "medium", label: "Medium" },
89 { value: "high", label: "High", isDefault: true },
90 { value: "max", label: "Max" },
91 ],
92} as const;
93
94const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [55const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
56 {
57 slug: "claude-fable-5",
58 name: "Claude Fable 5",
59 isCustom: false,
60 capabilities: createModelCapabilities({
61 optionDescriptors: [
62 buildSelectOptionDescriptor({
63 id: "effort",
64 label: "Reasoning",
65 options: [
66 { value: "low", label: "Low" },
67 { value: "medium", label: "Medium" },
68 { value: "high", label: "High", isDefault: true },
69 { value: "xhigh", label: "Extra High" },
70 { value: "max", label: "Max" },
71 { value: "ultracode", label: "Ultracode" },
72 { value: "ultrathink", label: "Ultrathink" },
73 ],
74 promptInjectedValues: ["ultrathink"],
75 }),
76 buildSelectOptionDescriptor({
77 id: "contextWindow",
78 label: "Context Window",
79 options: [
80 { value: "200k", label: "200k", isDefault: true },
81 { value: "1m", label: "1M" },
82 ],
83 }),
84 ],
85 }),
86 },
95 {87 {
96 slug: "claude-opus-4-8",88 slug: "claude-opus-4-8",
97 name: "Claude Opus 4.8",89 name: "Claude Opus 4.8",
...@@ -101,9 +93,21 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [...@@ -101,9 +93,21 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
101 buildSelectOptionDescriptor({93 buildSelectOptionDescriptor({
102 id: "effort",94 id: "effort",
103 label: "Reasoning",95 label: "Reasoning",
104 options: CLAUDE_EFFORT_OPTIONS.opus48,96 options: [
97 { value: "low", label: "Low" },
98 { value: "medium", label: "Medium" },
99 { value: "high", label: "High", isDefault: true },
100 { value: "xhigh", label: "Extra High" },
101 { value: "max", label: "Max" },
102 { value: "ultracode", label: "Ultracode" },
103 { value: "ultrathink", label: "Ultrathink" },
104 ],
105 promptInjectedValues: ["ultrathink"],105 promptInjectedValues: ["ultrathink"],
106 }),106 }),
107 buildBooleanOptionDescriptor({
108 id: "fastMode",
109 label: "Fast Mode",
110 }),
107 buildSelectOptionDescriptor({111 buildSelectOptionDescriptor({
108 id: "contextWindow",112 id: "contextWindow",
109 label: "Context Window",113 label: "Context Window",
...@@ -124,9 +128,20 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [...@@ -124,9 +128,20 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
124 buildSelectOptionDescriptor({128 buildSelectOptionDescriptor({
125 id: "effort",129 id: "effort",
126 label: "Reasoning",130 label: "Reasoning",
127 options: CLAUDE_EFFORT_OPTIONS.opus47,131 options: [
132 { value: "low", label: "Low" },
133 { value: "medium", label: "Medium" },
134 { value: "high", label: "High" },
135 { value: "xhigh", label: "Extra High", isDefault: true },
136 { value: "max", label: "Max" },
137 { value: "ultrathink", label: "Ultrathink" },
138 ],
128 promptInjectedValues: ["ultrathink"],139 promptInjectedValues: ["ultrathink"],
129 }),140 }),
141 buildBooleanOptionDescriptor({
142 id: "fastMode",
143 label: "Fast Mode",
144 }),
130 buildSelectOptionDescriptor({145 buildSelectOptionDescriptor({
131 id: "contextWindow",146 id: "contextWindow",
132 label: "Context Window",147 label: "Context Window",
...@@ -147,7 +162,13 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [...@@ -147,7 +162,13 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
147 buildSelectOptionDescriptor({162 buildSelectOptionDescriptor({
148 id: "effort",163 id: "effort",
149 label: "Reasoning",164 label: "Reasoning",
150 options: CLAUDE_EFFORT_OPTIONS.opus46,165 options: [
166 { value: "low", label: "Low" },
167 { value: "medium", label: "Medium" },
168 { value: "high", label: "High", isDefault: true },
169 { value: "max", label: "Max" },
170 { value: "ultrathink", label: "Ultrathink" },
171 ],
151 promptInjectedValues: ["ultrathink"],172 promptInjectedValues: ["ultrathink"],
152 }),173 }),
153 buildBooleanOptionDescriptor({174 buildBooleanOptionDescriptor({
...@@ -174,7 +195,12 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [...@@ -174,7 +195,12 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
174 buildSelectOptionDescriptor({195 buildSelectOptionDescriptor({
175 id: "effort",196 id: "effort",
176 label: "Reasoning",197 label: "Reasoning",
177 options: CLAUDE_EFFORT_OPTIONS.opus45,198 options: [
199 { value: "low", label: "Low" },
200 { value: "medium", label: "Medium" },
201 { value: "high", label: "High", isDefault: true },
202 { value: "max", label: "Max" },
203 ],
178 }),204 }),
179 buildBooleanOptionDescriptor({205 buildBooleanOptionDescriptor({
180 id: "fastMode",206 id: "fastMode",
...@@ -192,7 +218,13 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [...@@ -192,7 +218,13 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
192 buildSelectOptionDescriptor({218 buildSelectOptionDescriptor({
193 id: "effort",219 id: "effort",
194 label: "Reasoning",220 label: "Reasoning",
195 options: CLAUDE_EFFORT_OPTIONS.sonnet46,221 options: [
222 { value: "low", label: "Low" },
223 { value: "medium", label: "Medium" },
224 { value: "high", label: "High", isDefault: true },
225 { value: "max", label: "Max" },
226 { value: "ultrathink", label: "Ultrathink" },
227 ],
196 promptInjectedValues: ["ultrathink"],228 promptInjectedValues: ["ultrathink"],
197 }),229 }),
198 buildSelectOptionDescriptor({230 buildSelectOptionDescriptor({
...@@ -221,6 +253,10 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [...@@ -221,6 +253,10 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
221 },253 },
222];254];
223255
256function supportsClaudeFable5(version: string | null | undefined): boolean {
257 return version ? compareSemverVersions(version, MINIMUM_CLAUDE_FABLE_5_VERSION) >= 0 : false;
258}
259
224function supportsClaudeOpus48(version: string | null | undefined): boolean {260function supportsClaudeOpus48(version: string | null | undefined): boolean {
225 return version ? compareSemverVersions(version, MINIMUM_CLAUDE_OPUS_4_8_VERSION) >= 0 : false;261 return version ? compareSemverVersions(version, MINIMUM_CLAUDE_OPUS_4_8_VERSION) >= 0 : false;
226}262}
...@@ -233,6 +269,9 @@ function getBuiltInClaudeModelsForVersion(...@@ -233,6 +269,9 @@ function getBuiltInClaudeModelsForVersion(
233 version: string | null | undefined,269 version: string | null | undefined,
234): ReadonlyArray<ServerProviderModel> {270): ReadonlyArray<ServerProviderModel> {
235 return BUILT_IN_MODELS.filter((model) => {271 return BUILT_IN_MODELS.filter((model) => {
272 if (model.slug === "claude-fable-5") {
273 return supportsClaudeFable5(version);
274 }
236 if (model.slug === "claude-opus-4-8") {275 if (model.slug === "claude-opus-4-8") {
237 return supportsClaudeOpus48(version);276 return supportsClaudeOpus48(version);
238 }277 }
...@@ -243,6 +282,11 @@ function getBuiltInClaudeModelsForVersion(...@@ -243,6 +282,11 @@ function getBuiltInClaudeModelsForVersion(
243 });282 });
244}283}
245284
285function formatClaudeFable5UpgradeMessage(version: string | null): string {
286 const versionLabel = version ? `v${version}` : "the installed version";
287 return `Claude Code ${versionLabel} is too old for Claude Fable 5. Upgrade to v${MINIMUM_CLAUDE_FABLE_5_VERSION} or newer to access it.`;
288}
289
246function formatClaudeOpus48UpgradeMessage(version: string | null): string {290function formatClaudeOpus48UpgradeMessage(version: string | null): string {
247 const versionLabel = version ? `v${version}` : "the installed version";291 const versionLabel = version ? `v${version}` : "the installed version";
248 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.`;292 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(...@@ -294,7 +338,7 @@ export function normalizeClaudeCliEffort(
294 if (effort === "ultracode") {338 if (effort === "ultracode") {
295 return "xhigh";339 return "xhigh";
296 }340 }
297 if (effort === "xhigh" && model !== "claude-opus-4-8") {341 if (effort === "xhigh" && model !== "claude-fable-5" && model !== "claude-opus-4-8") {
298 return "max";342 return "max";
299 }343 }
300 if (effort === "max" && model === "claude-sonnet-4-6") {344 if (effort === "max" && model === "claude-sonnet-4-6") {
...@@ -688,11 +732,13 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(...@@ -688,11 +732,13 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
688 claudeSettings.customModels,732 claudeSettings.customModels,
689 DEFAULT_CLAUDE_MODEL_CAPABILITIES,733 DEFAULT_CLAUDE_MODEL_CAPABILITIES,
690 );734 );
691 const versionUpgradeMessage = supportsClaudeOpus48(parsedVersion)735 const versionUpgradeMessage = supportsClaudeFable5(parsedVersion)
692 ? undefined736 ? undefined
693 : supportsClaudeOpus47(parsedVersion)737 : supportsClaudeOpus48(parsedVersion)
694 ? formatClaudeOpus48UpgradeMessage(parsedVersion)738 ? formatClaudeFable5UpgradeMessage(parsedVersion)
695 : formatClaudeOpus47UpgradeMessage(parsedVersion);739 : supportsClaudeOpus47(parsedVersion)
740 ? formatClaudeOpus48UpgradeMessage(parsedVersion)
741 : formatClaudeOpus47UpgradeMessage(parsedVersion);
696742
697 const capabilities = resolveCapabilities743 const capabilities = resolveCapabilities
698 ? yield* resolveCapabilities(claudeSettings).pipe(Effect.orElseSucceed(() => undefined))744 ? yield* resolveCapabilities(claudeSettings).pipe(Effect.orElseSucceed(() => undefined))
apps/server/src/provider/Layers/ProviderRegistry.test.ts+56
...@@ -1367,6 +1367,62 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T...@@ -1367,6 +1367,62 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
1367 ),1367 ),
1368 );1368 );
13691369
1370 it.effect("includes Claude Fable 5 on supported Claude Code versions", () =>
1371 Effect.gen(function* () {
1372 const status = yield* checkClaudeProviderStatus(
1373 defaultClaudeSettings,
1374 claudeCapabilities(),
1375 );
1376 const fable5 = status.models.find((model) => model.slug === "claude-fable-5");
1377 assert.strictEqual(fable5?.name, "Claude Fable 5");
1378 }).pipe(
1379 Effect.provide(
1380 mockSpawnerLayer((args) => {
1381 const joined = args.join(" ");
1382 if (joined === "--version") return { stdout: "2.1.169\n", stderr: "", code: 0 };
1383 if (joined === "auth status")
1384 return {
1385 stdout: '{"loggedIn":true,"authMethod":"claude.ai"}\n',
1386 stderr: "",
1387 code: 0,
1388 };
1389 throw new Error(`Unexpected args: ${joined}`);
1390 }),
1391 ),
1392 ),
1393 );
1394
1395 it.effect("hides Claude Fable 5 on older Claude Code versions", () =>
1396 Effect.gen(function* () {
1397 const status = yield* checkClaudeProviderStatus(
1398 defaultClaudeSettings,
1399 claudeCapabilities(),
1400 );
1401 assert.strictEqual(
1402 status.models.some((model) => model.slug === "claude-fable-5"),
1403 false,
1404 );
1405 assert.strictEqual(
1406 status.message,
1407 "Claude Code v2.1.168 is too old for Claude Fable 5. Upgrade to v2.1.169 or newer to access it.",
1408 );
1409 }).pipe(
1410 Effect.provide(
1411 mockSpawnerLayer((args) => {
1412 const joined = args.join(" ");
1413 if (joined === "--version") return { stdout: "2.1.168\n", stderr: "", code: 0 };
1414 if (joined === "auth status")
1415 return {
1416 stdout: '{"loggedIn":true,"authMethod":"claude.ai"}\n',
1417 stderr: "",
1418 code: 0,
1419 };
1420 throw new Error(`Unexpected args: ${joined}`);
1421 }),
1422 ),
1423 ),
1424 );
1425
1370 it.effect(1426 it.effect(
1371 "includes Claude Opus 4.7 with xhigh as the default effort on supported versions",1427 "includes Claude Opus 4.7 with xhigh as the default effort on supported versions",
1372 () =>1428 () =>