Skip to content

Commit ce15434

Browse files
thomaslittleclaudet3dotgg
authored andcommitted
Add Claude Opus 5 model (pingdotgg#4472)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Theo Browne <me@t3.gg> (cherry picked from commit 41a430a)
1 parent 6fbf9d5 commit ce15434

5 files changed

Lines changed: 144 additions & 10 deletions

File tree

apps/server/src/provider/Layers/ClaudeAdapter.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,29 @@ describe("ClaudeAdapterLive", () => {
532532
);
533533
});
534534

535+
it.effect("preserves xhigh effort for Claude Opus 5", () => {
536+
const harness = makeHarness();
537+
return Effect.gen(function* () {
538+
const adapter = yield* ClaudeAdapter;
539+
yield* adapter.startSession({
540+
threadId: THREAD_ID,
541+
provider: ProviderDriverKind.make("claudeAgent"),
542+
modelSelection: createModelSelection(
543+
ProviderInstanceId.make("claudeAgent"),
544+
"claude-opus-5",
545+
[{ id: "effort", value: "xhigh" }],
546+
),
547+
runtimeMode: "full-access",
548+
});
549+
550+
const createInput = harness.getLastCreateQueryInput();
551+
assert.equal(createInput?.options.effort, "xhigh");
552+
}).pipe(
553+
Effect.provideService(Random.Random, makeDeterministicRandomService()),
554+
Effect.provide(harness.layer),
555+
);
556+
});
557+
535558
it.effect("falls back to default effort when unsupported max is requested for Sonnet 4.6", () => {
536559
const harness = makeHarness();
537560
return Effect.gen(function* () {

apps/server/src/provider/Layers/ClaudeProvider.ts

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const CLAUDE_PRESENTATION = {
5151
displayName: "Claude",
5252
showInteractionModeToggle: true,
5353
} as const;
54+
const MINIMUM_CLAUDE_OPUS_5_VERSION = "2.1.219";
5455
const MINIMUM_CLAUDE_FABLE_5_VERSION = "2.1.169";
5556
const MINIMUM_CLAUDE_SONNET_5_VERSION = "2.1.197";
5657
const MINIMUM_CLAUDE_OPUS_4_8_VERSION = "2.1.154";
@@ -151,6 +152,42 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
151152
],
152153
}),
153154
},
155+
{
156+
slug: "claude-opus-5",
157+
name: "Claude Opus 5",
158+
isCustom: false,
159+
capabilities: createModelCapabilities({
160+
optionDescriptors: [
161+
buildSelectOptionDescriptor({
162+
id: "effort",
163+
label: "Reasoning",
164+
options: [
165+
{ value: "low", label: "Low" },
166+
{ value: "medium", label: "Medium" },
167+
{ value: "high", label: "High", isDefault: true },
168+
{ value: "xhigh", label: "Extra High" },
169+
{ value: "max", label: "Max" },
170+
{ value: "ultracode", label: "Ultracode" },
171+
{ value: "ultrathink", label: "Ultrathink" },
172+
],
173+
promptInjectedValues: ["ultrathink"],
174+
}),
175+
buildBooleanOptionDescriptor({
176+
id: "fastMode",
177+
label: "Fast Mode",
178+
}),
179+
buildSelectOptionDescriptor({
180+
id: "contextWindow",
181+
label: "Context Window",
182+
// Claude Code selects the 1M variant explicitly (`claude-opus-5[1m]`).
183+
options: [
184+
{ value: "200k", label: "200k" },
185+
{ value: "1m", label: "1M", isDefault: true },
186+
],
187+
}),
188+
],
189+
}),
190+
},
154191
{
155192
slug: "claude-opus-4-8",
156193
name: "Claude Opus 4.8",
@@ -334,6 +371,10 @@ function withClaudeSonnet5ContextWindowSelector(
334371
});
335372
}
336373

374+
function supportsClaudeOpus5(version: string | null | undefined): boolean {
375+
return version ? compareSemverVersions(version, MINIMUM_CLAUDE_OPUS_5_VERSION) >= 0 : false;
376+
}
377+
337378
function supportsClaudeFable5(version: string | null | undefined): boolean {
338379
return version ? compareSemverVersions(version, MINIMUM_CLAUDE_FABLE_5_VERSION) >= 0 : false;
339380
}
@@ -354,6 +395,9 @@ export function getBuiltInClaudeModelsForVersion(
354395
version: string | null | undefined,
355396
): ReadonlyArray<ServerProviderModel> {
356397
return BUILT_IN_MODELS.filter((model) => {
398+
if (model.slug === "claude-opus-5") {
399+
return supportsClaudeOpus5(version);
400+
}
357401
if (model.slug === "claude-fable-5") {
358402
return supportsClaudeFable5(version);
359403
}
@@ -370,6 +414,11 @@ export function getBuiltInClaudeModelsForVersion(
370414
});
371415
}
372416

417+
function formatClaudeOpus5UpgradeMessage(version: string | null): string {
418+
const versionLabel = version ? `v${version}` : "the installed version";
419+
return `Claude Code ${versionLabel} is too old for Claude Opus 5. Upgrade to v${MINIMUM_CLAUDE_OPUS_5_VERSION} or newer to access it.`;
420+
}
421+
373422
function formatClaudeFable5UpgradeMessage(version: string | null): string {
374423
const versionLabel = version ? `v${version}` : "the installed version";
375424
return `Claude Code ${versionLabel} is too old for Claude Fable 5. Upgrade to v${MINIMUM_CLAUDE_FABLE_5_VERSION} or newer to access it.`;
@@ -434,6 +483,7 @@ export function normalizeClaudeCliEffort(
434483
if (
435484
effort === "xhigh" &&
436485
model !== "claude-fable-5" &&
486+
model !== "claude-opus-5" &&
437487
model !== "claude-opus-4-8" &&
438488
model !== "claude-sonnet-5"
439489
) {
@@ -920,15 +970,17 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
920970
claudeSettings.customModels,
921971
DEFAULT_CLAUDE_MODEL_CAPABILITIES,
922972
);
923-
const versionUpgradeMessage = supportsClaudeSonnet5(parsedVersion)
973+
const versionUpgradeMessage = supportsClaudeOpus5(parsedVersion)
924974
? undefined
925-
: supportsClaudeFable5(parsedVersion)
926-
? formatClaudeSonnet5UpgradeMessage(parsedVersion)
927-
: supportsClaudeOpus48(parsedVersion)
928-
? formatClaudeFable5UpgradeMessage(parsedVersion)
929-
: supportsClaudeOpus47(parsedVersion)
930-
? formatClaudeOpus48UpgradeMessage(parsedVersion)
931-
: formatClaudeOpus47UpgradeMessage(parsedVersion);
975+
: supportsClaudeSonnet5(parsedVersion)
976+
? formatClaudeOpus5UpgradeMessage(parsedVersion)
977+
: supportsClaudeFable5(parsedVersion)
978+
? formatClaudeSonnet5UpgradeMessage(parsedVersion)
979+
: supportsClaudeOpus48(parsedVersion)
980+
? formatClaudeFable5UpgradeMessage(parsedVersion)
981+
: supportsClaudeOpus47(parsedVersion)
982+
? formatClaudeOpus48UpgradeMessage(parsedVersion)
983+
: formatClaudeOpus47UpgradeMessage(parsedVersion);
932984

933985
const capabilities = resolveCapabilities
934986
? yield* resolveCapabilities(claudeSettings).pipe(Effect.orElseSucceed(() => undefined))

apps/server/src/provider/Layers/ProviderRegistry.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1873,6 +1873,62 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te
18731873
),
18741874
);
18751875

1876+
it.effect("includes Claude Opus 5 on supported Claude Code versions", () =>
1877+
Effect.gen(function* () {
1878+
const status = yield* checkClaudeProviderStatus(
1879+
defaultClaudeSettings,
1880+
claudeCapabilities(),
1881+
);
1882+
const opus5 = status.models.find((model) => model.slug === "claude-opus-5");
1883+
assert.strictEqual(opus5?.name, "Claude Opus 5");
1884+
}).pipe(
1885+
Effect.provide(
1886+
mockSpawnerLayer((args) => {
1887+
const joined = args.join(" ");
1888+
if (joined === "--version") return { stdout: "2.1.219\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+
1901+
it.effect("hides Claude Opus 5 on older Claude Code versions", () =>
1902+
Effect.gen(function* () {
1903+
const status = yield* checkClaudeProviderStatus(
1904+
defaultClaudeSettings,
1905+
claudeCapabilities(),
1906+
);
1907+
assert.strictEqual(
1908+
status.models.some((model) => model.slug === "claude-opus-5"),
1909+
false,
1910+
);
1911+
assert.strictEqual(
1912+
status.message,
1913+
"Claude Code v2.1.218 is too old for Claude Opus 5. Upgrade to v2.1.219 or newer to access it.",
1914+
);
1915+
}).pipe(
1916+
Effect.provide(
1917+
mockSpawnerLayer((args) => {
1918+
const joined = args.join(" ");
1919+
if (joined === "--version") return { stdout: "2.1.218\n", stderr: "", code: 0 };
1920+
if (joined === "auth status")
1921+
return {
1922+
stdout: '{"loggedIn":true,"authMethod":"claude.ai"}\n',
1923+
stderr: "",
1924+
code: 0,
1925+
};
1926+
throw new Error(`Unexpected args: ${joined}`);
1927+
}),
1928+
),
1929+
),
1930+
);
1931+
18761932
it.effect("includes Claude Fable 5 on supported Claude Code versions", () =>
18771933
Effect.gen(function* () {
18781934
const status = yield* checkClaudeProviderStatus(

packages/contracts/src/model.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,10 @@ export const MODEL_SLUG_ALIASES_BY_PROVIDER: Partial<
178178
"gpt-5.3-spark": "gpt-5.3-codex-spark",
179179
},
180180
[CLAUDE_DRIVER_KIND]: {
181-
opus: "claude-opus-4-8",
181+
opus: "claude-opus-5",
182+
"opus-5": "claude-opus-5",
183+
"claude-opus-5.0": "claude-opus-5",
184+
"claude-opus-5-0": "claude-opus-5",
182185
"opus-4.8": "claude-opus-4-8",
183186
"claude-opus-4.8": "claude-opus-4-8",
184187
"opus-4.7": "claude-opus-4-7",

packages/shared/src/model.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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
});

0 commit comments

Comments
 (0)