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", () => {...@@ -525,6 +525,29 @@ describe("ClaudeAdapterLive", () => {
525 );525 );
526 });526 });
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
528 it.effect("preserves xhigh effort for Claude Fable 5", () => {551 it.effect("preserves xhigh effort for Claude Fable 5", () => {
529 const harness = makeHarness();552 const harness = makeHarness();
530 return Effect.gen(function* () {553 return Effect.gen(function* () {
apps/server/src/provider/Layers/ClaudeProvider.ts+59-8
...@@ -51,12 +51,48 @@ const CLAUDE_PRESENTATION = {...@@ -51,12 +51,48 @@ const CLAUDE_PRESENTATION = {
51 displayName: "Claude",51 displayName: "Claude",
52 showInteractionModeToggle: true,52 showInteractionModeToggle: true,
53} as const;53} as const;
54const MINIMUM_CLAUDE_FABLE_5_1_VERSION = "2.1.257";
54const MINIMUM_CLAUDE_OPUS_5_VERSION = "2.1.219";55const MINIMUM_CLAUDE_OPUS_5_VERSION = "2.1.219";
55const MINIMUM_CLAUDE_FABLE_5_VERSION = "2.1.169";56const MINIMUM_CLAUDE_FABLE_5_VERSION = "2.1.169";
56const MINIMUM_CLAUDE_OPUS_4_8_VERSION = "2.1.154";57const MINIMUM_CLAUDE_OPUS_4_8_VERSION = "2.1.154";
57const MINIMUM_CLAUDE_OPUS_4_7_VERSION = "2.1.111";58const MINIMUM_CLAUDE_OPUS_4_7_VERSION = "2.1.111";
5859
59const CLAUDE_MODEL_CATALOG: ReadonlyArray<ServerProviderModel> = [60const 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 },
60 {96 {
61 slug: "claude-fable-5",97 slug: "claude-fable-5",
62 name: "Claude Fable 5",98 name: "Claude Fable 5",
...@@ -325,6 +361,10 @@ const CLAUDE_MODEL_CATALOG: ReadonlyArray<ServerProviderModel> = [...@@ -325,6 +361,10 @@ const CLAUDE_MODEL_CATALOG: ReadonlyArray<ServerProviderModel> = [
325// so the catalog itself carries no `isLegacy` flags.361// so the catalog itself carries no `isLegacy` flags.
326const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = CLAUDE_MODEL_CATALOG;362const 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
328function supportsClaudeOpus5(version: string | null | undefined): boolean {368function supportsClaudeOpus5(version: string | null | undefined): boolean {
329 return version ? compareSemverVersions(version, MINIMUM_CLAUDE_OPUS_5_VERSION) >= 0 : false;369 return version ? compareSemverVersions(version, MINIMUM_CLAUDE_OPUS_5_VERSION) >= 0 : false;
330}370}
...@@ -345,6 +385,9 @@ function getBuiltInClaudeModelsForVersion(...@@ -345,6 +385,9 @@ function getBuiltInClaudeModelsForVersion(
345 version: string | null | undefined,385 version: string | null | undefined,
346): ReadonlyArray<ServerProviderModel> {386): ReadonlyArray<ServerProviderModel> {
347 return BUILT_IN_MODELS.filter((model) => {387 return BUILT_IN_MODELS.filter((model) => {
388 if (model.slug === "claude-fable-5-1") {
389 return supportsClaudeFable51(version);
390 }
348 if (model.slug === "claude-opus-5") {391 if (model.slug === "claude-opus-5") {
349 return supportsClaudeOpus5(version);392 return supportsClaudeOpus5(version);
350 }393 }
...@@ -361,6 +404,11 @@ function getBuiltInClaudeModelsForVersion(...@@ -361,6 +404,11 @@ function getBuiltInClaudeModelsForVersion(
361 });404 });
362}405}
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
364function formatClaudeOpus5UpgradeMessage(version: string | null): string {412function formatClaudeOpus5UpgradeMessage(version: string | null): string {
365 const versionLabel = version ? `v${version}` : "the installed version";413 const versionLabel = version ? `v${version}` : "the installed version";
366 return `Claude Code ${versionLabel} is too old for Claude Opus 5. Upgrade to v${MINIMUM_CLAUDE_OPUS_5_VERSION} or newer to access it.`;414 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(...@@ -424,6 +472,7 @@ export function normalizeClaudeCliEffort(
424 }472 }
425 if (473 if (
426 effort === "xhigh" &&474 effort === "xhigh" &&
475 model !== "claude-fable-5-1" &&
427 model !== "claude-fable-5" &&476 model !== "claude-fable-5" &&
428 model !== "claude-opus-5" &&477 model !== "claude-opus-5" &&
429 model !== "claude-opus-4-8" &&478 model !== "claude-opus-4-8" &&
...@@ -913,15 +962,17 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(...@@ -913,15 +962,17 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
913 claudeSettings.customModels,962 claudeSettings.customModels,
914 DEFAULT_CLAUDE_MODEL_CAPABILITIES,963 DEFAULT_CLAUDE_MODEL_CAPABILITIES,
915 );964 );
916 const versionUpgradeMessage = supportsClaudeOpus5(parsedVersion)965 const versionUpgradeMessage = supportsClaudeFable51(parsedVersion)
917 ? undefined966 ? undefined
918 : supportsClaudeFable5(parsedVersion)967 : supportsClaudeOpus5(parsedVersion)
919 ? formatClaudeOpus5UpgradeMessage(parsedVersion)968 ? formatClaudeFable51UpgradeMessage(parsedVersion)
920 : supportsClaudeOpus48(parsedVersion)969 : supportsClaudeFable5(parsedVersion)
921 ? formatClaudeFable5UpgradeMessage(parsedVersion)970 ? formatClaudeOpus5UpgradeMessage(parsedVersion)
922 : supportsClaudeOpus47(parsedVersion)971 : supportsClaudeOpus48(parsedVersion)
923 ? formatClaudeOpus48UpgradeMessage(parsedVersion)972 ? formatClaudeFable5UpgradeMessage(parsedVersion)
924 : formatClaudeOpus47UpgradeMessage(parsedVersion);973 : supportsClaudeOpus47(parsedVersion)
974 ? formatClaudeOpus48UpgradeMessage(parsedVersion)
975 : formatClaudeOpus47UpgradeMessage(parsedVersion);
925976
926 const capabilities = resolveCapabilities977 const capabilities = resolveCapabilities
927 ? yield* resolveCapabilities(claudeSettings).pipe(Effect.orElseSucceed(() => undefined))978 ? 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...@@ -2132,6 +2132,62 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te
2132 ),2132 ),
2133 );2133 );
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
2135 it.effect("hides Claude Fable 5 on older Claude Code versions", () =>2191 it.effect("hides Claude Fable 5 on older Claude Code versions", () =>
2136 Effect.gen(function* () {2192 Effect.gen(function* () {
2137 const status = yield* checkClaudeProviderStatus(2193 const status = yield* checkClaudeProviderStatus(
apps/server/src/provider/ModelManifest.test.ts+9-5
...@@ -43,12 +43,16 @@ describe("isLegacyModel (bundled manifest)", () => {...@@ -43,12 +43,16 @@ describe("isLegacyModel (bundled manifest)", () => {
4343
44 it("keeps only the Claude 5 family out of legacy models", () => {44 it("keeps only the Claude 5 family out of legacy models", () => {
45 assert.deepStrictEqual(45 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 ]),
50 [46 [
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],
52 ["claude-opus-5", false],56 ["claude-opus-5", false],
53 ["claude-sonnet-5", false],57 ["claude-sonnet-5", false],
54 ["claude-opus-4-8", true],58 ["claude-opus-4-8", true],
apps/server/src/provider/model-manifest.json+1-1
...@@ -8,6 +8,6 @@...@@ -8,6 +8,6 @@
8 "gpt-daybreak-blue-latest",8 "gpt-daybreak-blue-latest",
9 "gpt-daybreak-red-latest"9 "gpt-daybreak-red-latest"
10 ],10 ],
11 "claudeAgent": ["claude-fable-5", "claude-opus-5", "claude-sonnet-5"]11 "claudeAgent": ["claude-fable-5-1", "claude-opus-5", "claude-sonnet-5"]
12 }12 }
13}13}
packages/contracts/src/model.ts+3
...@@ -177,6 +177,9 @@ export const MODEL_SLUG_ALIASES_BY_PROVIDER: Partial<...@@ -177,6 +177,9 @@ export const MODEL_SLUG_ALIASES_BY_PROVIDER: Partial<
177 "gpt-5.3-spark": "gpt-5.3-codex-spark",177 "gpt-5.3-spark": "gpt-5.3-codex-spark",
178 },178 },
179 [CLAUDE_DRIVER_KIND]: {179 [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",
180 opus: "claude-opus-5",183 opus: "claude-opus-5",
181 "opus-5": "claude-opus-5",184 "opus-5": "claude-opus-5",
182 "claude-opus-5.0": "claude-opus-5",185 "claude-opus-5.0": "claude-opus-5",
packages/shared/src/model.test.ts+1
...@@ -152,6 +152,7 @@ describe("model slug normalization", () => {...@@ -152,6 +152,7 @@ describe("model slug normalization", () => {
152 it("preserves exact custom slugs instead of expanding provider aliases", () => {152 it("preserves exact custom slugs instead of expanding provider aliases", () => {
153 const claude = ProviderDriverKind.make("claudeAgent");153 const claude = ProviderDriverKind.make("claudeAgent");
154154
155 expect(normalizeModelSlug("fable", claude)).toBe("claude-fable-5-1");
155 expect(normalizeModelSlug("opus", claude)).toBe("claude-opus-5");156 expect(normalizeModelSlug("opus", claude)).toBe("claude-opus-5");
156 expect(normalizeCustomModelSlug(" opus ")).toBe("opus");157 expect(normalizeCustomModelSlug(" opus ")).toBe("opus");
157 });158 });