| 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,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 | 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; |
| 54 | const MINIMUM_CLAUDE_FABLE_5_1_VERSION = "2.1.257"; | ||
| 54 | const MINIMUM_CLAUDE_OPUS_5_VERSION = "2.1.219"; | 55 | const MINIMUM_CLAUDE_OPUS_5_VERSION = "2.1.219"; |
| 55 | const MINIMUM_CLAUDE_FABLE_5_VERSION = "2.1.169"; | 56 | const MINIMUM_CLAUDE_FABLE_5_VERSION = "2.1.169"; |
| 56 | const MINIMUM_CLAUDE_OPUS_4_8_VERSION = "2.1.154"; | 57 | const MINIMUM_CLAUDE_OPUS_4_8_VERSION = "2.1.154"; |
| 57 | const MINIMUM_CLAUDE_OPUS_4_7_VERSION = "2.1.111"; | 58 | const MINIMUM_CLAUDE_OPUS_4_7_VERSION = "2.1.111"; |
| 58 | 59 | ||
| 59 | const CLAUDE_MODEL_CATALOG: ReadonlyArray<ServerProviderModel> = [ | 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 | 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. |
| 326 | const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = CLAUDE_MODEL_CATALOG; | 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 | function supportsClaudeOpus5(version: string | null | undefined): boolean { | 368 | function 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 | } |
| 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 | function formatClaudeOpus5UpgradeMessage(version: string | null): string { | 412 | function 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 | ? undefined | 966 | ? 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); | ||
| 925 | 976 | ||
| 926 | const capabilities = resolveCapabilities | 977 | 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 | ); |
| 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 | 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)", () => { |
| 43 | 43 | ||
| 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"); |
| 154 | 154 | ||
| 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 | }); |