authorgravatar for tomlit@gmail.comtomlit <tomlit@gmail.com> 2026-07-24 16:16:33-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-24 13:16:33-07:00
log41a430a88e8dde9c428f59d54dd328aa6a66a8fd
tree259dbd1d7fb03bce4d2efa0cdcd5ea442ed8bc8c
parentece05087a70e94efcd57441337fa1249559362ba
signature Signed by PGP key B5690EEEBB952194

Add Claude Opus 5 model (#4472)

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,6 +530,29 @@ describe("ClaudeAdapterLive", () => {
530 );530 );
531 });531 });
532532
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 it.effect("falls back to default effort when unsupported max is requested for Sonnet 4.6", () => {556 it.effect("falls back to default effort when unsupported max is requested for Sonnet 4.6", () => {
534 const harness = makeHarness();557 const harness = makeHarness();
535 return Effect.gen(function* () {558 return Effect.gen(function* () {
apps/server/src/provider/Layers/ClaudeProvider.ts+58-6
...@@ -51,6 +51,7 @@ const CLAUDE_PRESENTATION = {...@@ -51,6 +51,7 @@ const CLAUDE_PRESENTATION = {
51 displayName: "Claude",51 displayName: "Claude",
52 showInteractionModeToggle: true,52 showInteractionModeToggle: true,
53} as const;53} as const;
54const MINIMUM_CLAUDE_OPUS_5_VERSION = "2.1.219";
54const MINIMUM_CLAUDE_FABLE_5_VERSION = "2.1.169";55const MINIMUM_CLAUDE_FABLE_5_VERSION = "2.1.169";
55const MINIMUM_CLAUDE_OPUS_4_8_VERSION = "2.1.154";56const MINIMUM_CLAUDE_OPUS_4_8_VERSION = "2.1.154";
56const MINIMUM_CLAUDE_OPUS_4_7_VERSION = "2.1.111";57const MINIMUM_CLAUDE_OPUS_4_7_VERSION = "2.1.111";
...@@ -87,6 +88,42 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [...@@ -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 slug: "claude-opus-4-8",128 slug: "claude-opus-4-8",
92 name: "Claude Opus 4.8",129 name: "Claude Opus 4.8",
...@@ -272,6 +309,10 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [...@@ -272,6 +309,10 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
272 },309 },
273];310];
274311
312function supportsClaudeOpus5(version: string | null | undefined): boolean {
313 return version ? compareSemverVersions(version, MINIMUM_CLAUDE_OPUS_5_VERSION) >= 0 : false;
314}
315
275function supportsClaudeFable5(version: string | null | undefined): boolean {316function supportsClaudeFable5(version: string | null | undefined): boolean {
276 return version ? compareSemverVersions(version, MINIMUM_CLAUDE_FABLE_5_VERSION) >= 0 : false;317 return version ? compareSemverVersions(version, MINIMUM_CLAUDE_FABLE_5_VERSION) >= 0 : false;
277}318}
...@@ -288,6 +329,9 @@ function getBuiltInClaudeModelsForVersion(...@@ -288,6 +329,9 @@ function getBuiltInClaudeModelsForVersion(
288 version: string | null | undefined,329 version: string | null | undefined,
289): ReadonlyArray<ServerProviderModel> {330): ReadonlyArray<ServerProviderModel> {
290 return BUILT_IN_MODELS.filter((model) => {331 return BUILT_IN_MODELS.filter((model) => {
332 if (model.slug === "claude-opus-5") {
333 return supportsClaudeOpus5(version);
334 }
291 if (model.slug === "claude-fable-5") {335 if (model.slug === "claude-fable-5") {
292 return supportsClaudeFable5(version);336 return supportsClaudeFable5(version);
293 }337 }
...@@ -301,6 +345,11 @@ function getBuiltInClaudeModelsForVersion(...@@ -301,6 +345,11 @@ function getBuiltInClaudeModelsForVersion(
301 });345 });
302}346}
303347
348function 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
304function formatClaudeFable5UpgradeMessage(version: string | null): string {353function formatClaudeFable5UpgradeMessage(version: string | null): string {
305 const versionLabel = version ? `v${version}` : "the installed version";354 const versionLabel = version ? `v${version}` : "the installed version";
306 return `Claude Code ${versionLabel} is too old for Claude Fable 5. Upgrade to v${MINIMUM_CLAUDE_FABLE_5_VERSION} or newer to access it.`;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,6 +409,7 @@ export function normalizeClaudeCliEffort(
360 if (409 if (
361 effort === "xhigh" &&410 effort === "xhigh" &&
362 model !== "claude-fable-5" &&411 model !== "claude-fable-5" &&
412 model !== "claude-opus-5" &&
363 model !== "claude-opus-4-8" &&413 model !== "claude-opus-4-8" &&
364 model !== "claude-sonnet-5"414 model !== "claude-sonnet-5"
365 ) {415 ) {
...@@ -837,13 +887,15 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(...@@ -837,13 +887,15 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
837 claudeSettings.customModels,887 claudeSettings.customModels,
838 DEFAULT_CLAUDE_MODEL_CAPABILITIES,888 DEFAULT_CLAUDE_MODEL_CAPABILITIES,
839 );889 );
840 const versionUpgradeMessage = supportsClaudeFable5(parsedVersion)890 const versionUpgradeMessage = supportsClaudeOpus5(parsedVersion)
841 ? undefined891 ? undefined
842 : supportsClaudeOpus48(parsedVersion)892 : supportsClaudeFable5(parsedVersion)
843 ? formatClaudeFable5UpgradeMessage(parsedVersion)893 ? formatClaudeOpus5UpgradeMessage(parsedVersion)
844 : supportsClaudeOpus47(parsedVersion)894 : supportsClaudeOpus48(parsedVersion)
845 ? formatClaudeOpus48UpgradeMessage(parsedVersion)895 ? formatClaudeFable5UpgradeMessage(parsedVersion)
846 : formatClaudeOpus47UpgradeMessage(parsedVersion);896 : supportsClaudeOpus47(parsedVersion)
897 ? formatClaudeOpus48UpgradeMessage(parsedVersion)
898 : formatClaudeOpus47UpgradeMessage(parsedVersion);
847899
848 const capabilities = resolveCapabilities900 const capabilities = resolveCapabilities
849 ? yield* resolveCapabilities(claudeSettings).pipe(Effect.orElseSucceed(() => undefined))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,6 +1842,62 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te
1842 ),1842 ),
1843 );1843 );
18441844
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 it.effect("includes Claude Fable 5 on supported Claude Code versions", () =>1901 it.effect("includes Claude Fable 5 on supported Claude Code versions", () =>
1846 Effect.gen(function* () {1902 Effect.gen(function* () {
1847 const status = yield* checkClaudeProviderStatus(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,7 +176,10 @@ export const MODEL_SLUG_ALIASES_BY_PROVIDER: Partial<
176 "gpt-5.3-spark": "gpt-5.3-codex-spark",176 "gpt-5.3-spark": "gpt-5.3-codex-spark",
177 },177 },
178 [CLAUDE_DRIVER_KIND]: {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 "opus-4.8": "claude-opus-4-8",183 "opus-4.8": "claude-opus-4-8",
181 "claude-opus-4.8": "claude-opus-4-8",184 "claude-opus-4.8": "claude-opus-4-8",
182 "opus-4.7": "claude-opus-4-7",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,7 +151,7 @@ describe("model slug normalization", () => {
151 it("preserves exact custom slugs instead of expanding provider aliases", () => {151 it("preserves exact custom slugs instead of expanding provider aliases", () => {
152 const claude = ProviderDriverKind.make("claudeAgent");152 const claude = ProviderDriverKind.make("claudeAgent");
153153
154 expect(normalizeModelSlug("opus", claude)).toBe("claude-opus-4-8");154 expect(normalizeModelSlug("opus", claude)).toBe("claude-opus-5");
155 expect(normalizeCustomModelSlug(" opus ")).toBe("opus");155 expect(normalizeCustomModelSlug(" opus ")).toBe("opus");
156 });156 });
157});157});