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", () => {
447447 );
448448 });
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
450473 it.effect("falls back to default effort when unsupported max is requested for Sonnet 4.6", () => {
451474 const harness = makeHarness();
452475 return Effect.gen(function* () {
apps/server/src/provider/Layers/ClaudeProvider.ts+96-50
......@@ -48,50 +48,42 @@ const CLAUDE_PRESENTATION = {
4848 displayName: "Claude",
4949 showInteractionModeToggle: true,
5050} as const;
51const MINIMUM_CLAUDE_FABLE_5_VERSION = "2.1.169";
5152const MINIMUM_CLAUDE_OPUS_4_8_VERSION = "2.1.154";
5253const 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
9455const 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 },
9587 {
9688 slug: "claude-opus-4-8",
9789 name: "Claude Opus 4.8",
......@@ -101,9 +93,21 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
10193 buildSelectOptionDescriptor({
10294 id: "effort",
10395 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 ],
105105 promptInjectedValues: ["ultrathink"],
106106 }),
107 buildBooleanOptionDescriptor({
108 id: "fastMode",
109 label: "Fast Mode",
110 }),
107111 buildSelectOptionDescriptor({
108112 id: "contextWindow",
109113 label: "Context Window",
......@@ -124,9 +128,20 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
124128 buildSelectOptionDescriptor({
125129 id: "effort",
126130 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 ],
128139 promptInjectedValues: ["ultrathink"],
129140 }),
141 buildBooleanOptionDescriptor({
142 id: "fastMode",
143 label: "Fast Mode",
144 }),
130145 buildSelectOptionDescriptor({
131146 id: "contextWindow",
132147 label: "Context Window",
......@@ -147,7 +162,13 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
147162 buildSelectOptionDescriptor({
148163 id: "effort",
149164 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 ],
151172 promptInjectedValues: ["ultrathink"],
152173 }),
153174 buildBooleanOptionDescriptor({
......@@ -174,7 +195,12 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
174195 buildSelectOptionDescriptor({
175196 id: "effort",
176197 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 ],
178204 }),
179205 buildBooleanOptionDescriptor({
180206 id: "fastMode",
......@@ -192,7 +218,13 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
192218 buildSelectOptionDescriptor({
193219 id: "effort",
194220 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 ],
196228 promptInjectedValues: ["ultrathink"],
197229 }),
198230 buildSelectOptionDescriptor({
......@@ -221,6 +253,10 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
221253 },
222254];
223255
256function supportsClaudeFable5(version: string | null | undefined): boolean {
257 return version ? compareSemverVersions(version, MINIMUM_CLAUDE_FABLE_5_VERSION) >= 0 : false;
258}
259
224260function supportsClaudeOpus48(version: string | null | undefined): boolean {
225261 return version ? compareSemverVersions(version, MINIMUM_CLAUDE_OPUS_4_8_VERSION) >= 0 : false;
226262}
......@@ -233,6 +269,9 @@ function getBuiltInClaudeModelsForVersion(
233269 version: string | null | undefined,
234270): ReadonlyArray<ServerProviderModel> {
235271 return BUILT_IN_MODELS.filter((model) => {
272 if (model.slug === "claude-fable-5") {
273 return supportsClaudeFable5(version);
274 }
236275 if (model.slug === "claude-opus-4-8") {
237276 return supportsClaudeOpus48(version);
238277 }
......@@ -243,6 +282,11 @@ function getBuiltInClaudeModelsForVersion(
243282 });
244283}
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
246290function formatClaudeOpus48UpgradeMessage(version: string | null): string {
247291 const versionLabel = version ? `v${version}` : "the installed version";
248292 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(
294338 if (effort === "ultracode") {
295339 return "xhigh";
296340 }
297 if (effort === "xhigh" && model !== "claude-opus-4-8") {
341 if (effort === "xhigh" && model !== "claude-fable-5" && model !== "claude-opus-4-8") {
298342 return "max";
299343 }
300344 if (effort === "max" && model === "claude-sonnet-4-6") {
......@@ -688,11 +732,13 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
688732 claudeSettings.customModels,
689733 DEFAULT_CLAUDE_MODEL_CAPABILITIES,
690734 );
691 const versionUpgradeMessage = supportsClaudeOpus48(parsedVersion)
735 const versionUpgradeMessage = supportsClaudeFable5(parsedVersion)
692736 ? undefined
693 : supportsClaudeOpus47(parsedVersion)
694 ? formatClaudeOpus48UpgradeMessage(parsedVersion)
695 : formatClaudeOpus47UpgradeMessage(parsedVersion);
737 : supportsClaudeOpus48(parsedVersion)
738 ? formatClaudeFable5UpgradeMessage(parsedVersion)
739 : supportsClaudeOpus47(parsedVersion)
740 ? formatClaudeOpus48UpgradeMessage(parsedVersion)
741 : formatClaudeOpus47UpgradeMessage(parsedVersion);
696742
697743 const capabilities = resolveCapabilities
698744 ? 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
13671367 ),
13681368 );
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
13701426 it.effect(
13711427 "includes Claude Opus 4.7 with xhigh as the default effort on supported versions",
13721428 () =>