| author | |
| committer | |
| log | c17d02cff98f2e7b590d4c3a5775d1faa47c2e7b |
| tree | 62808984fcefa3f9e3dc01ab214bd1e862b4e640 |
| parent | 643b21edaa08eb1d19d633dbb0b275164bfa60be |
| signature | Signed by PGP key B5690EEEBB952194 |
7 files changed, 152 insertions(+), 14 deletions(-)
apps/server/src/provider/Layers/ClaudeAdapter.test.ts+23| ... | ... | @@ -525,6 +525,29 @@ describe("ClaudeAdapterLive", () => { |
| 525 | 525 | ); |
| 526 | 526 | }); |
| 527 | 527 | |
| 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 | 551 | it.effect("preserves xhigh effort for Claude Fable 5", () => { |
| 529 | 552 | const harness = makeHarness(); |
| 530 | 553 | return Effect.gen(function* () { |
apps/server/src/provider/Layers/ClaudeProvider.ts+59-8| ... | ... | @@ -51,12 +51,48 @@ const CLAUDE_PRESENTATION = { |
| 51 | 51 | displayName: "Claude", |
| 52 | 52 | showInteractionModeToggle: true, |
| 53 | 53 | } as const; |
| 54 | const MINIMUM_CLAUDE_FABLE_5_1_VERSION = "2.1.257"; | |
| 54 | 55 | const MINIMUM_CLAUDE_OPUS_5_VERSION = "2.1.219"; |
| 55 | 56 | const MINIMUM_CLAUDE_FABLE_5_VERSION = "2.1.169"; |
| 56 | 57 | const MINIMUM_CLAUDE_OPUS_4_8_VERSION = "2.1.154"; |
| 57 | 58 | const MINIMUM_CLAUDE_OPUS_4_7_VERSION = "2.1.111"; |
| 58 | 59 | |
| 59 | 60 | const 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 | 97 | slug: "claude-fable-5", |
| 62 | 98 | name: "Claude Fable 5", |
| ... | ... | @@ -325,6 +361,10 @@ const CLAUDE_MODEL_CATALOG: ReadonlyArray<ServerProviderModel> = [ |
| 325 | 361 | // so the catalog itself carries no `isLegacy` flags. |
| 326 | 362 | const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = CLAUDE_MODEL_CATALOG; |
| 327 | 363 | |
| 364 | function supportsClaudeFable51(version: string | null | undefined): boolean { | |
| 365 | return version ? compareSemverVersions(version, MINIMUM_CLAUDE_FABLE_5_1_VERSION) >= 0 : false; | |
| 366 | } | |
| 367 | ||
| 328 | 368 | function supportsClaudeOpus5(version: string | null | undefined): boolean { |
| 329 | 369 | return version ? compareSemverVersions(version, MINIMUM_CLAUDE_OPUS_5_VERSION) >= 0 : false; |
| 330 | 370 | } |
| ... | ... | @@ -345,6 +385,9 @@ function getBuiltInClaudeModelsForVersion( |
| 345 | 385 | version: string | null | undefined, |
| 346 | 386 | ): ReadonlyArray<ServerProviderModel> { |
| 347 | 387 | return BUILT_IN_MODELS.filter((model) => { |
| 388 | if (model.slug === "claude-fable-5-1") { | |
| 389 | return supportsClaudeFable51(version); | |
| 390 | } | |
| 348 | 391 | if (model.slug === "claude-opus-5") { |
| 349 | 392 | return supportsClaudeOpus5(version); |
| 350 | 393 | } |
| ... | ... | @@ -361,6 +404,11 @@ function getBuiltInClaudeModelsForVersion( |
| 361 | 404 | }); |
| 362 | 405 | } |
| 363 | 406 | |
| 407 | function 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 | ||
| 364 | 412 | function formatClaudeOpus5UpgradeMessage(version: string | null): string { |
| 365 | 413 | const versionLabel = version ? `v${version}` : "the installed version"; |
| 366 | 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 | 472 | } |
| 425 | 473 | if ( |
| 426 | 474 | effort === "xhigh" && |
| 475 | model !== "claude-fable-5-1" && | |
| 427 | 476 | model !== "claude-fable-5" && |
| 428 | 477 | model !== "claude-opus-5" && |
| 429 | 478 | model !== "claude-opus-4-8" && |
| ... | ... | @@ -913,15 +962,17 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( |
| 913 | 962 | claudeSettings.customModels, |
| 914 | 963 | DEFAULT_CLAUDE_MODEL_CAPABILITIES, |
| 915 | 964 | ); |
| 916 | const versionUpgradeMessage = supportsClaudeOpus5(parsedVersion) | |
| 965 | const versionUpgradeMessage = supportsClaudeFable51(parsedVersion) | |
| 917 | 966 | ? 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); | |
| 925 | 976 | |
| 926 | 977 | const capabilities = resolveCapabilities |
| 927 | 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 | 2132 | ), |
| 2133 | 2133 | ); |
| 2134 | 2134 | |
| 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 | 2191 | it.effect("hides Claude Fable 5 on older Claude Code versions", () => |
| 2136 | 2192 | Effect.gen(function* () { |
| 2137 | 2193 | const status = yield* checkClaudeProviderStatus( |
apps/server/src/provider/ModelManifest.test.ts+9-5| ... | ... | @@ -43,12 +43,16 @@ describe("isLegacyModel (bundled manifest)", () => { |
| 43 | 43 | |
| 44 | 44 | it("keeps only the Claude 5 family out of legacy models", () => { |
| 45 | 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 | 56 | ["claude-opus-5", false], |
| 53 | 57 | ["claude-sonnet-5", false], |
| 54 | 58 | ["claude-opus-4-8", true], |
apps/server/src/provider/model-manifest.json+1-1| ... | ... | @@ -8,6 +8,6 @@ |
| 8 | 8 | "gpt-daybreak-blue-latest", |
| 9 | 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 | 177 | "gpt-5.3-spark": "gpt-5.3-codex-spark", |
| 178 | 178 | }, |
| 179 | 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 | 183 | opus: "claude-opus-5", |
| 181 | 184 | "opus-5": "claude-opus-5", |
| 182 | 185 | "claude-opus-5.0": "claude-opus-5", |
packages/shared/src/model.test.ts+1| ... | ... | @@ -152,6 +152,7 @@ describe("model slug normalization", () => { |
| 152 | 152 | it("preserves exact custom slugs instead of expanding provider aliases", () => { |
| 153 | 153 | const claude = ProviderDriverKind.make("claudeAgent"); |
| 154 | 154 | |
| 155 | expect(normalizeModelSlug("fable", claude)).toBe("claude-fable-5-1"); | |
| 155 | 156 | expect(normalizeModelSlug("opus", claude)).toBe("claude-opus-5"); |
| 156 | 157 | expect(normalizeCustomModelSlug(" opus ")).toBe("opus"); |
| 157 | 158 | }); |