authorgravatar for 85814106+q1@users.noreply.github.commic <85814106+q1@users.noreply.github.com> 2026-09-01 12:20:56-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-01 12:20:56-07:00
logc17d02cff98f2e7b590d4c3a5775d1faa47c2e7b
tree62808984fcefa3f9e3dc01ab214bd1e862b4e640
parent643b21edaa08eb1d19d633dbb0b275164bfa60be
signature Signed by PGP key B5690EEEBB952194

feat(claude): add Claude Fable 5.1 model (#9078)


7 files changed, 152 insertions(+), 14 deletions(-)

apps/server/src/provider/Layers/ClaudeAdapter.test.ts+23
......@@ -525,6 +525,29 @@ describe("ClaudeAdapterLive", () => {
525525 );
526526 });
527527
528 it.effect("preserves xhigh effort for Claude Fable 5.1", () => {
529 const harness = makeHarness();
530 return Effect.gen(function* () {
531 const adapter = yield* ClaudeAdapter;
532 yield* adapter.startSession({
533 threadId: THREAD_ID,
534 provider: ProviderDriverKind.make("claudeAgent"),
535 modelSelection: createModelSelection(
536 ProviderInstanceId.make("claudeAgent"),
537 "claude-fable-5-1",
538 [{ id: "effort", value: "xhigh" }],
539 ),
540 runtimeMode: "full-access",
541 });
542
543 const createInput = harness.getLastCreateQueryInput();
544 assert.equal(createInput?.options.effort, "xhigh");
545 }).pipe(
546 Effect.provideService(Random.Random, makeDeterministicRandomService()),
547 Effect.provide(harness.layer),
548 );
549 });
550
528551 it.effect("preserves xhigh effort for Claude Fable 5", () => {
529552 const harness = makeHarness();
530553 return Effect.gen(function* () {
apps/server/src/provider/Layers/ClaudeProvider.ts+59-8
......@@ -51,12 +51,48 @@ const CLAUDE_PRESENTATION = {
5151 displayName: "Claude",
5252 showInteractionModeToggle: true,
5353} as const;
54const MINIMUM_CLAUDE_FABLE_5_1_VERSION = "2.1.257";
5455const MINIMUM_CLAUDE_OPUS_5_VERSION = "2.1.219";
5556const MINIMUM_CLAUDE_FABLE_5_VERSION = "2.1.169";
5657const MINIMUM_CLAUDE_OPUS_4_8_VERSION = "2.1.154";
5758const MINIMUM_CLAUDE_OPUS_4_7_VERSION = "2.1.111";
5859
5960const CLAUDE_MODEL_CATALOG: ReadonlyArray<ServerProviderModel> = [
61 {
62 slug: "claude-fable-5-1",
63 name: "Claude Fable 5.1",
64 isCustom: false,
65 capabilities: createModelCapabilities({
66 optionDescriptors: [
67 buildSelectOptionDescriptor({
68 id: "effort",
69 label: "Reasoning",
70 options: [
71 { value: "low", label: "Low" },
72 { value: "medium", label: "Medium" },
73 { value: "high", label: "High", isDefault: true },
74 { value: "xhigh", label: "Extra High" },
75 { value: "max", label: "Max" },
76 {
77 value: "ultracode",
78 label: "Ultracode",
79 description: "xhigh effort plus multi-agent workflow orchestration",
80 },
81 { value: "ultrathink", label: "Ultrathink" },
82 ],
83 promptInjectedValues: ["ultrathink"],
84 }),
85 buildSelectOptionDescriptor({
86 id: "contextWindow",
87 label: "Context Window",
88 options: [
89 { value: "200k", label: "200k" },
90 { value: "1m", label: "1M", isDefault: true },
91 ],
92 }),
93 ],
94 }),
95 },
6096 {
6197 slug: "claude-fable-5",
6298 name: "Claude Fable 5",
......@@ -325,6 +361,10 @@ const CLAUDE_MODEL_CATALOG: ReadonlyArray<ServerProviderModel> = [
325361// so the catalog itself carries no `isLegacy` flags.
326362const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = CLAUDE_MODEL_CATALOG;
327363
364function supportsClaudeFable51(version: string | null | undefined): boolean {
365 return version ? compareSemverVersions(version, MINIMUM_CLAUDE_FABLE_5_1_VERSION) >= 0 : false;
366}
367
328368function supportsClaudeOpus5(version: string | null | undefined): boolean {
329369 return version ? compareSemverVersions(version, MINIMUM_CLAUDE_OPUS_5_VERSION) >= 0 : false;
330370}
......@@ -345,6 +385,9 @@ function getBuiltInClaudeModelsForVersion(
345385 version: string | null | undefined,
346386): ReadonlyArray<ServerProviderModel> {
347387 return BUILT_IN_MODELS.filter((model) => {
388 if (model.slug === "claude-fable-5-1") {
389 return supportsClaudeFable51(version);
390 }
348391 if (model.slug === "claude-opus-5") {
349392 return supportsClaudeOpus5(version);
350393 }
......@@ -361,6 +404,11 @@ function getBuiltInClaudeModelsForVersion(
361404 });
362405}
363406
407function formatClaudeFable51UpgradeMessage(version: string | null): string {
408 const versionLabel = version ? `v${version}` : "the installed version";
409 return `Claude Code ${versionLabel} is too old for Claude Fable 5.1. Upgrade to v${MINIMUM_CLAUDE_FABLE_5_1_VERSION} or newer to access it.`;
410}
411
364412function formatClaudeOpus5UpgradeMessage(version: string | null): string {
365413 const versionLabel = version ? `v${version}` : "the installed version";
366414 return `Claude Code ${versionLabel} is too old for Claude Opus 5. Upgrade to v${MINIMUM_CLAUDE_OPUS_5_VERSION} or newer to access it.`;
......@@ -424,6 +472,7 @@ export function normalizeClaudeCliEffort(
424472 }
425473 if (
426474 effort === "xhigh" &&
475 model !== "claude-fable-5-1" &&
427476 model !== "claude-fable-5" &&
428477 model !== "claude-opus-5" &&
429478 model !== "claude-opus-4-8" &&
......@@ -913,15 +962,17 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
913962 claudeSettings.customModels,
914963 DEFAULT_CLAUDE_MODEL_CAPABILITIES,
915964 );
916 const versionUpgradeMessage = supportsClaudeOpus5(parsedVersion)
965 const versionUpgradeMessage = supportsClaudeFable51(parsedVersion)
917966 ? undefined
918 : supportsClaudeFable5(parsedVersion)
919 ? formatClaudeOpus5UpgradeMessage(parsedVersion)
920 : supportsClaudeOpus48(parsedVersion)
921 ? formatClaudeFable5UpgradeMessage(parsedVersion)
922 : supportsClaudeOpus47(parsedVersion)
923 ? formatClaudeOpus48UpgradeMessage(parsedVersion)
924 : formatClaudeOpus47UpgradeMessage(parsedVersion);
967 : supportsClaudeOpus5(parsedVersion)
968 ? formatClaudeFable51UpgradeMessage(parsedVersion)
969 : supportsClaudeFable5(parsedVersion)
970 ? formatClaudeOpus5UpgradeMessage(parsedVersion)
971 : supportsClaudeOpus48(parsedVersion)
972 ? formatClaudeFable5UpgradeMessage(parsedVersion)
973 : supportsClaudeOpus47(parsedVersion)
974 ? formatClaudeOpus48UpgradeMessage(parsedVersion)
975 : formatClaudeOpus47UpgradeMessage(parsedVersion);
925976
926977 const capabilities = resolveCapabilities
927978 ? yield* resolveCapabilities(claudeSettings).pipe(Effect.orElseSucceed(() => undefined))
apps/server/src/provider/Layers/ProviderRegistry.test.ts+56
......@@ -2132,6 +2132,62 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te
21322132 ),
21332133 );
21342134
2135 it.effect("includes Claude Fable 5.1 on supported Claude Code versions", () =>
2136 Effect.gen(function* () {
2137 const status = yield* checkClaudeProviderStatus(
2138 defaultClaudeSettings,
2139 claudeCapabilities(),
2140 );
2141 const fable51 = status.models.find((model) => model.slug === "claude-fable-5-1");
2142 assert.strictEqual(fable51?.name, "Claude Fable 5.1");
2143 }).pipe(
2144 Effect.provide(
2145 mockSpawnerLayer((args) => {
2146 const joined = args.join(" ");
2147 if (joined === "--version") return { stdout: "2.1.257\n", stderr: "", code: 0 };
2148 if (joined === "auth status")
2149 return {
2150 stdout: '{"loggedIn":true,"authMethod":"claude.ai"}\n',
2151 stderr: "",
2152 code: 0,
2153 };
2154 throw new Error(`Unexpected args: ${joined}`);
2155 }),
2156 ),
2157 ),
2158 );
2159
2160 it.effect("hides Claude Fable 5.1 on older Claude Code versions", () =>
2161 Effect.gen(function* () {
2162 const status = yield* checkClaudeProviderStatus(
2163 defaultClaudeSettings,
2164 claudeCapabilities(),
2165 );
2166 assert.strictEqual(
2167 status.models.some((model) => model.slug === "claude-fable-5-1"),
2168 false,
2169 );
2170 assert.strictEqual(
2171 status.message,
2172 "Claude Code v2.1.256 is too old for Claude Fable 5.1. Upgrade to v2.1.257 or newer to access it.",
2173 );
2174 }).pipe(
2175 Effect.provide(
2176 mockSpawnerLayer((args) => {
2177 const joined = args.join(" ");
2178 if (joined === "--version") return { stdout: "2.1.256\n", stderr: "", code: 0 };
2179 if (joined === "auth status")
2180 return {
2181 stdout: '{"loggedIn":true,"authMethod":"claude.ai"}\n',
2182 stderr: "",
2183 code: 0,
2184 };
2185 throw new Error(`Unexpected args: ${joined}`);
2186 }),
2187 ),
2188 ),
2189 );
2190
21352191 it.effect("hides Claude Fable 5 on older Claude Code versions", () =>
21362192 Effect.gen(function* () {
21372193 const status = yield* checkClaudeProviderStatus(
apps/server/src/provider/ModelManifest.test.ts+9-5
......@@ -43,12 +43,16 @@ describe("isLegacyModel (bundled manifest)", () => {
4343
4444 it("keeps only the Claude 5 family out of legacy models", () => {
4545 assert.deepStrictEqual(
46 ["claude-fable-5", "claude-opus-5", "claude-sonnet-5", "claude-opus-4-8"].map((model) => [
47 model,
48 isLegacyModel(BUNDLED_MODEL_MANIFEST, CLAUDE, model),
49 ]),
5046 [
51 ["claude-fable-5", false],
47 "claude-fable-5-1",
48 "claude-fable-5",
49 "claude-opus-5",
50 "claude-sonnet-5",
51 "claude-opus-4-8",
52 ].map((model) => [model, isLegacyModel(BUNDLED_MODEL_MANIFEST, CLAUDE, model)]),
53 [
54 ["claude-fable-5-1", false],
55 ["claude-fable-5", true],
5256 ["claude-opus-5", false],
5357 ["claude-sonnet-5", false],
5458 ["claude-opus-4-8", true],
apps/server/src/provider/model-manifest.json+1-1
......@@ -8,6 +8,6 @@
88 "gpt-daybreak-blue-latest",
99 "gpt-daybreak-red-latest"
1010 ],
11 "claudeAgent": ["claude-fable-5", "claude-opus-5", "claude-sonnet-5"]
11 "claudeAgent": ["claude-fable-5-1", "claude-opus-5", "claude-sonnet-5"]
1212 }
1313}
packages/contracts/src/model.ts+3
......@@ -177,6 +177,9 @@ export const MODEL_SLUG_ALIASES_BY_PROVIDER: Partial<
177177 "gpt-5.3-spark": "gpt-5.3-codex-spark",
178178 },
179179 [CLAUDE_DRIVER_KIND]: {
180 fable: "claude-fable-5-1",
181 "fable-5.1": "claude-fable-5-1",
182 "claude-fable-5.1": "claude-fable-5-1",
180183 opus: "claude-opus-5",
181184 "opus-5": "claude-opus-5",
182185 "claude-opus-5.0": "claude-opus-5",
packages/shared/src/model.test.ts+1
......@@ -152,6 +152,7 @@ describe("model slug normalization", () => {
152152 it("preserves exact custom slugs instead of expanding provider aliases", () => {
153153 const claude = ProviderDriverKind.make("claudeAgent");
154154
155 expect(normalizeModelSlug("fable", claude)).toBe("claude-fable-5-1");
155156 expect(normalizeModelSlug("opus", claude)).toBe("claude-opus-5");
156157 expect(normalizeCustomModelSlug(" opus ")).toBe("opus");
157158 });