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", () => {
530530 );
531531 });
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
533556 it.effect("falls back to default effort when unsupported max is requested for Sonnet 4.6", () => {
534557 const harness = makeHarness();
535558 return Effect.gen(function* () {
apps/server/src/provider/Layers/ClaudeProvider.ts+58-6
......@@ -51,6 +51,7 @@ const CLAUDE_PRESENTATION = {
5151 displayName: "Claude",
5252 showInteractionModeToggle: true,
5353} as const;
54const MINIMUM_CLAUDE_OPUS_5_VERSION = "2.1.219";
5455const MINIMUM_CLAUDE_FABLE_5_VERSION = "2.1.169";
5556const MINIMUM_CLAUDE_OPUS_4_8_VERSION = "2.1.154";
5657const MINIMUM_CLAUDE_OPUS_4_7_VERSION = "2.1.111";
......@@ -87,6 +88,42 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
8788 ],
8889 }),
8990 },
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 },
90127 {
91128 slug: "claude-opus-4-8",
92129 name: "Claude Opus 4.8",
......@@ -272,6 +309,10 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
272309 },
273310];
274311
312function supportsClaudeOpus5(version: string | null | undefined): boolean {
313 return version ? compareSemverVersions(version, MINIMUM_CLAUDE_OPUS_5_VERSION) >= 0 : false;
314}
315
275316function supportsClaudeFable5(version: string | null | undefined): boolean {
276317 return version ? compareSemverVersions(version, MINIMUM_CLAUDE_FABLE_5_VERSION) >= 0 : false;
277318}
......@@ -288,6 +329,9 @@ function getBuiltInClaudeModelsForVersion(
288329 version: string | null | undefined,
289330): ReadonlyArray<ServerProviderModel> {
290331 return BUILT_IN_MODELS.filter((model) => {
332 if (model.slug === "claude-opus-5") {
333 return supportsClaudeOpus5(version);
334 }
291335 if (model.slug === "claude-fable-5") {
292336 return supportsClaudeFable5(version);
293337 }
......@@ -301,6 +345,11 @@ function getBuiltInClaudeModelsForVersion(
301345 });
302346}
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
304353function formatClaudeFable5UpgradeMessage(version: string | null): string {
305354 const versionLabel = version ? `v${version}` : "the installed version";
306355 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(
360409 if (
361410 effort === "xhigh" &&
362411 model !== "claude-fable-5" &&
412 model !== "claude-opus-5" &&
363413 model !== "claude-opus-4-8" &&
364414 model !== "claude-sonnet-5"
365415 ) {
......@@ -837,13 +887,15 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
837887 claudeSettings.customModels,
838888 DEFAULT_CLAUDE_MODEL_CAPABILITIES,
839889 );
840 const versionUpgradeMessage = supportsClaudeFable5(parsedVersion)
890 const versionUpgradeMessage = supportsClaudeOpus5(parsedVersion)
841891 ? 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);
847899
848900 const capabilities = resolveCapabilities
849901 ? 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
18421842 ),
18431843 );
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
18451901 it.effect("includes Claude Fable 5 on supported Claude Code versions", () =>
18461902 Effect.gen(function* () {
18471903 const status = yield* checkClaudeProviderStatus(
packages/contracts/src/model.ts+4-1
......@@ -176,7 +176,10 @@ export const MODEL_SLUG_ALIASES_BY_PROVIDER: Partial<
176176 "gpt-5.3-spark": "gpt-5.3-codex-spark",
177177 },
178178 [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",
180183 "opus-4.8": "claude-opus-4-8",
181184 "claude-opus-4.8": "claude-opus-4-8",
182185 "opus-4.7": "claude-opus-4-7",
packages/shared/src/model.test.ts+1-1
......@@ -151,7 +151,7 @@ describe("model slug normalization", () => {
151151 it("preserves exact custom slugs instead of expanding provider aliases", () => {
152152 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");
155155 expect(normalizeCustomModelSlug(" opus ")).toBe("opus");
156156 });
157157});