| author | |
| committer | |
| log | 41a430a88e8dde9c428f59d54dd328aa6a66a8fd |
| tree | 259dbd1d7fb03bce4d2efa0cdcd5ea442ed8bc8c |
| parent | ece05087a70e94efcd57441337fa1249559362ba |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Theo Browne <me@t3.gg>5 files changed, 142 insertions(+), 8 deletions(-)
apps/server/src/provider/Layers/ClaudeAdapter.test.ts+23| ... | ... | @@ -530,6 +530,29 @@ describe("ClaudeAdapterLive", () => { |
| 530 | 530 | ); |
| 531 | 531 | }); |
| 532 | 532 | |
| 533 | it.effect("preserves xhigh effort for Claude Opus 5", () => { | |
| 534 | const harness = makeHarness(); | |
| 535 | return Effect.gen(function* () { | |
| 536 | const adapter = yield* ClaudeAdapter; | |
| 537 | yield* adapter.startSession({ | |
| 538 | threadId: THREAD_ID, | |
| 539 | provider: ProviderDriverKind.make("claudeAgent"), | |
| 540 | modelSelection: createModelSelection( | |
| 541 | ProviderInstanceId.make("claudeAgent"), | |
| 542 | "claude-opus-5", | |
| 543 | [{ id: "effort", value: "xhigh" }], | |
| 544 | ), | |
| 545 | runtimeMode: "full-access", | |
| 546 | }); | |
| 547 | ||
| 548 | const createInput = harness.getLastCreateQueryInput(); | |
| 549 | assert.equal(createInput?.options.effort, "xhigh"); | |
| 550 | }).pipe( | |
| 551 | Effect.provideService(Random.Random, makeDeterministicRandomService()), | |
| 552 | Effect.provide(harness.layer), | |
| 553 | ); | |
| 554 | }); | |
| 555 | ||
| 533 | 556 | it.effect("falls back to default effort when unsupported max is requested for Sonnet 4.6", () => { |
| 534 | 557 | const harness = makeHarness(); |
| 535 | 558 | return Effect.gen(function* () { |
apps/server/src/provider/Layers/ClaudeProvider.ts+58-6| ... | ... | @@ -51,6 +51,7 @@ const CLAUDE_PRESENTATION = { |
| 51 | 51 | displayName: "Claude", |
| 52 | 52 | showInteractionModeToggle: true, |
| 53 | 53 | } as const; |
| 54 | const MINIMUM_CLAUDE_OPUS_5_VERSION = "2.1.219"; | |
| 54 | 55 | const MINIMUM_CLAUDE_FABLE_5_VERSION = "2.1.169"; |
| 55 | 56 | const MINIMUM_CLAUDE_OPUS_4_8_VERSION = "2.1.154"; |
| 56 | 57 | const MINIMUM_CLAUDE_OPUS_4_7_VERSION = "2.1.111"; |
| ... | ... | @@ -87,6 +88,42 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [ |
| 87 | 88 | ], |
| 88 | 89 | }), |
| 89 | 90 | }, |
| 91 | { | |
| 92 | slug: "claude-opus-5", | |
| 93 | name: "Claude Opus 5", | |
| 94 | isCustom: false, | |
| 95 | capabilities: createModelCapabilities({ | |
| 96 | optionDescriptors: [ | |
| 97 | buildSelectOptionDescriptor({ | |
| 98 | id: "effort", | |
| 99 | label: "Reasoning", | |
| 100 | options: [ | |
| 101 | { value: "low", label: "Low" }, | |
| 102 | { value: "medium", label: "Medium" }, | |
| 103 | { value: "high", label: "High", isDefault: true }, | |
| 104 | { value: "xhigh", label: "Extra High" }, | |
| 105 | { value: "max", label: "Max" }, | |
| 106 | { value: "ultracode", label: "Ultracode" }, | |
| 107 | { value: "ultrathink", label: "Ultrathink" }, | |
| 108 | ], | |
| 109 | promptInjectedValues: ["ultrathink"], | |
| 110 | }), | |
| 111 | buildBooleanOptionDescriptor({ | |
| 112 | id: "fastMode", | |
| 113 | label: "Fast Mode", | |
| 114 | }), | |
| 115 | buildSelectOptionDescriptor({ | |
| 116 | id: "contextWindow", | |
| 117 | label: "Context Window", | |
| 118 | // Claude Code selects the 1M variant explicitly (`claude-opus-5[1m]`). | |
| 119 | options: [ | |
| 120 | { value: "200k", label: "200k" }, | |
| 121 | { value: "1m", label: "1M", isDefault: true }, | |
| 122 | ], | |
| 123 | }), | |
| 124 | ], | |
| 125 | }), | |
| 126 | }, | |
| 90 | 127 | { |
| 91 | 128 | slug: "claude-opus-4-8", |
| 92 | 129 | name: "Claude Opus 4.8", |
| ... | ... | @@ -272,6 +309,10 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [ |
| 272 | 309 | }, |
| 273 | 310 | ]; |
| 274 | 311 | |
| 312 | function supportsClaudeOpus5(version: string | null | undefined): boolean { | |
| 313 | return version ? compareSemverVersions(version, MINIMUM_CLAUDE_OPUS_5_VERSION) >= 0 : false; | |
| 314 | } | |
| 315 | ||
| 275 | 316 | function supportsClaudeFable5(version: string | null | undefined): boolean { |
| 276 | 317 | return version ? compareSemverVersions(version, MINIMUM_CLAUDE_FABLE_5_VERSION) >= 0 : false; |
| 277 | 318 | } |
| ... | ... | @@ -288,6 +329,9 @@ function getBuiltInClaudeModelsForVersion( |
| 288 | 329 | version: string | null | undefined, |
| 289 | 330 | ): ReadonlyArray<ServerProviderModel> { |
| 290 | 331 | return BUILT_IN_MODELS.filter((model) => { |
| 332 | if (model.slug === "claude-opus-5") { | |
| 333 | return supportsClaudeOpus5(version); | |
| 334 | } | |
| 291 | 335 | if (model.slug === "claude-fable-5") { |
| 292 | 336 | return supportsClaudeFable5(version); |
| 293 | 337 | } |
| ... | ... | @@ -301,6 +345,11 @@ function getBuiltInClaudeModelsForVersion( |
| 301 | 345 | }); |
| 302 | 346 | } |
| 303 | 347 | |
| 348 | function formatClaudeOpus5UpgradeMessage(version: string | null): string { | |
| 349 | const versionLabel = version ? `v${version}` : "the installed version"; | |
| 350 | return `Claude Code ${versionLabel} is too old for Claude Opus 5. Upgrade to v${MINIMUM_CLAUDE_OPUS_5_VERSION} or newer to access it.`; | |
| 351 | } | |
| 352 | ||
| 304 | 353 | function formatClaudeFable5UpgradeMessage(version: string | null): string { |
| 305 | 354 | const versionLabel = version ? `v${version}` : "the installed version"; |
| 306 | 355 | return `Claude Code ${versionLabel} is too old for Claude Fable 5. Upgrade to v${MINIMUM_CLAUDE_FABLE_5_VERSION} or newer to access it.`; |
| ... | ... | @@ -360,6 +409,7 @@ export function normalizeClaudeCliEffort( |
| 360 | 409 | if ( |
| 361 | 410 | effort === "xhigh" && |
| 362 | 411 | model !== "claude-fable-5" && |
| 412 | model !== "claude-opus-5" && | |
| 363 | 413 | model !== "claude-opus-4-8" && |
| 364 | 414 | model !== "claude-sonnet-5" |
| 365 | 415 | ) { |
| ... | ... | @@ -837,13 +887,15 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( |
| 837 | 887 | claudeSettings.customModels, |
| 838 | 888 | DEFAULT_CLAUDE_MODEL_CAPABILITIES, |
| 839 | 889 | ); |
| 840 | const versionUpgradeMessage = supportsClaudeFable5(parsedVersion) | |
| 890 | const versionUpgradeMessage = supportsClaudeOpus5(parsedVersion) | |
| 841 | 891 | ? undefined |
| 842 | : supportsClaudeOpus48(parsedVersion) | |
| 843 | ? formatClaudeFable5UpgradeMessage(parsedVersion) | |
| 844 | : supportsClaudeOpus47(parsedVersion) | |
| 845 | ? formatClaudeOpus48UpgradeMessage(parsedVersion) | |
| 846 | : formatClaudeOpus47UpgradeMessage(parsedVersion); | |
| 892 | : supportsClaudeFable5(parsedVersion) | |
| 893 | ? formatClaudeOpus5UpgradeMessage(parsedVersion) | |
| 894 | : supportsClaudeOpus48(parsedVersion) | |
| 895 | ? formatClaudeFable5UpgradeMessage(parsedVersion) | |
| 896 | : supportsClaudeOpus47(parsedVersion) | |
| 897 | ? formatClaudeOpus48UpgradeMessage(parsedVersion) | |
| 898 | : formatClaudeOpus47UpgradeMessage(parsedVersion); | |
| 847 | 899 | |
| 848 | 900 | const capabilities = resolveCapabilities |
| 849 | 901 | ? yield* resolveCapabilities(claudeSettings).pipe(Effect.orElseSucceed(() => undefined)) |
apps/server/src/provider/Layers/ProviderRegistry.test.ts+56| ... | ... | @@ -1842,6 +1842,62 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te |
| 1842 | 1842 | ), |
| 1843 | 1843 | ); |
| 1844 | 1844 | |
| 1845 | it.effect("includes Claude Opus 5 on supported Claude Code versions", () => | |
| 1846 | Effect.gen(function* () { | |
| 1847 | const status = yield* checkClaudeProviderStatus( | |
| 1848 | defaultClaudeSettings, | |
| 1849 | claudeCapabilities(), | |
| 1850 | ); | |
| 1851 | const opus5 = status.models.find((model) => model.slug === "claude-opus-5"); | |
| 1852 | assert.strictEqual(opus5?.name, "Claude Opus 5"); | |
| 1853 | }).pipe( | |
| 1854 | Effect.provide( | |
| 1855 | mockSpawnerLayer((args) => { | |
| 1856 | const joined = args.join(" "); | |
| 1857 | if (joined === "--version") return { stdout: "2.1.219\n", stderr: "", code: 0 }; | |
| 1858 | if (joined === "auth status") | |
| 1859 | return { | |
| 1860 | stdout: '{"loggedIn":true,"authMethod":"claude.ai"}\n', | |
| 1861 | stderr: "", | |
| 1862 | code: 0, | |
| 1863 | }; | |
| 1864 | throw new Error(`Unexpected args: ${joined}`); | |
| 1865 | }), | |
| 1866 | ), | |
| 1867 | ), | |
| 1868 | ); | |
| 1869 | ||
| 1870 | it.effect("hides Claude Opus 5 on older Claude Code versions", () => | |
| 1871 | Effect.gen(function* () { | |
| 1872 | const status = yield* checkClaudeProviderStatus( | |
| 1873 | defaultClaudeSettings, | |
| 1874 | claudeCapabilities(), | |
| 1875 | ); | |
| 1876 | assert.strictEqual( | |
| 1877 | status.models.some((model) => model.slug === "claude-opus-5"), | |
| 1878 | false, | |
| 1879 | ); | |
| 1880 | assert.strictEqual( | |
| 1881 | status.message, | |
| 1882 | "Claude Code v2.1.218 is too old for Claude Opus 5. Upgrade to v2.1.219 or newer to access it.", | |
| 1883 | ); | |
| 1884 | }).pipe( | |
| 1885 | Effect.provide( | |
| 1886 | mockSpawnerLayer((args) => { | |
| 1887 | const joined = args.join(" "); | |
| 1888 | if (joined === "--version") return { stdout: "2.1.218\n", stderr: "", code: 0 }; | |
| 1889 | if (joined === "auth status") | |
| 1890 | return { | |
| 1891 | stdout: '{"loggedIn":true,"authMethod":"claude.ai"}\n', | |
| 1892 | stderr: "", | |
| 1893 | code: 0, | |
| 1894 | }; | |
| 1895 | throw new Error(`Unexpected args: ${joined}`); | |
| 1896 | }), | |
| 1897 | ), | |
| 1898 | ), | |
| 1899 | ); | |
| 1900 | ||
| 1845 | 1901 | it.effect("includes Claude Fable 5 on supported Claude Code versions", () => |
| 1846 | 1902 | Effect.gen(function* () { |
| 1847 | 1903 | const status = yield* checkClaudeProviderStatus( |
packages/contracts/src/model.ts+4-1| ... | ... | @@ -176,7 +176,10 @@ export const MODEL_SLUG_ALIASES_BY_PROVIDER: Partial< |
| 176 | 176 | "gpt-5.3-spark": "gpt-5.3-codex-spark", |
| 177 | 177 | }, |
| 178 | 178 | [CLAUDE_DRIVER_KIND]: { |
| 179 | opus: "claude-opus-4-8", | |
| 179 | opus: "claude-opus-5", | |
| 180 | "opus-5": "claude-opus-5", | |
| 181 | "claude-opus-5.0": "claude-opus-5", | |
| 182 | "claude-opus-5-0": "claude-opus-5", | |
| 180 | 183 | "opus-4.8": "claude-opus-4-8", |
| 181 | 184 | "claude-opus-4.8": "claude-opus-4-8", |
| 182 | 185 | "opus-4.7": "claude-opus-4-7", |
packages/shared/src/model.test.ts+1-1| ... | ... | @@ -151,7 +151,7 @@ describe("model slug normalization", () => { |
| 151 | 151 | it("preserves exact custom slugs instead of expanding provider aliases", () => { |
| 152 | 152 | const claude = ProviderDriverKind.make("claudeAgent"); |
| 153 | 153 | |
| 154 | expect(normalizeModelSlug("opus", claude)).toBe("claude-opus-4-8"); | |
| 154 | expect(normalizeModelSlug("opus", claude)).toBe("claude-opus-5"); | |
| 155 | 155 | expect(normalizeCustomModelSlug(" opus ")).toBe("opus"); |
| 156 | 156 | }); |
| 157 | 157 | }); |