Skip to content

Commit 83f0cc9

Browse files
authored
Add Claude Opus 4.8 support (#2849)
1 parent e6330ea commit 83f0cc9

6 files changed

Lines changed: 149 additions & 49 deletions

File tree

apps/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"test": "vitest run"
2424
},
2525
"dependencies": {
26-
"@anthropic-ai/claude-agent-sdk": "^0.2.111",
26+
"@anthropic-ai/claude-agent-sdk": "^0.3.154",
2727
"@effect/platform-bun": "catalog:",
2828
"@effect/platform-node": "catalog:",
2929
"@effect/platform-node-shared": "catalog:",

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import { ServerConfig } from "../../config.ts";
7070
import { makeClaudeEnvironment } from "../Drivers/ClaudeHome.ts";
7171
import {
7272
getClaudeModelCapabilities,
73+
isClaudeUltracodeEffort,
7374
normalizeClaudeCliEffort,
7475
resolveClaudeApiModelId,
7576
resolveClaudeEffort,
@@ -255,8 +256,11 @@ function normalizeClaudeStreamMessages(
255256
return squashed.length > 0 ? [squashed] : [];
256257
}
257258

258-
function getEffectiveClaudeAgentEffort(effort: string | null | undefined): ClaudeSdkEffort | null {
259-
const normalized = normalizeClaudeCliEffort(effort);
259+
function getEffectiveClaudeAgentEffort(
260+
effort: string | null | undefined,
261+
model: string | null | undefined,
262+
): ClaudeSdkEffort | null {
263+
const normalized = normalizeClaudeCliEffort(effort, model);
260264
return normalized ? (normalized as ClaudeSdkEffort) : null;
261265
}
262266

@@ -2908,7 +2912,8 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
29082912
const thinking = thinkingSupported
29092913
? getModelSelectionBooleanOptionValue(modelSelection, "thinking")
29102914
: undefined;
2911-
const effectiveEffort = getEffectiveClaudeAgentEffort(effort);
2915+
const ultracode = isClaudeUltracodeEffort(effort);
2916+
const effectiveEffort = getEffectiveClaudeAgentEffort(effort, modelSelection?.model);
29122917
const runtimeModeToPermission: Record<string, PermissionMode> = {
29132918
"auto-accept-edits": "acceptEdits",
29142919
"full-access": "bypassPermissions",
@@ -2917,15 +2922,16 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
29172922
const settings = {
29182923
...(typeof thinking === "boolean" ? { alwaysThinkingEnabled: thinking } : {}),
29192924
...(fastMode ? { fastMode: true } : {}),
2925+
...(ultracode ? { ultracode: true } : {}),
29202926
};
29212927
const queryOptions: ClaudeQueryOptions = {
29222928
...(input.cwd ? { cwd: input.cwd } : {}),
29232929
...(apiModelId ? { model: apiModelId } : {}),
29242930
pathToClaudeCodeExecutable: claudeBinaryPath,
29252931
systemPrompt: { type: "preset", preset: "claude_code" },
29262932
settingSources: [...CLAUDE_SETTING_SOURCES],
2927-
// The SDK type lags the CLI here: Opus 4.7 accepts `xhigh` even though
2928-
// the published `Options["effort"]` union currently stops at `max`.
2933+
// `ultracode` is a Claude Code setting, not an API effort level. It is
2934+
// normalized to `xhigh` above and paired with `settings.ultracode`.
29292935
...(effectiveEffort
29302936
? {
29312937
effort: effectiveEffort as unknown as NonNullable<ClaudeQueryOptions["effort"]>,

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

Lines changed: 111 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -48,25 +48,83 @@ const CLAUDE_PRESENTATION = {
4848
displayName: "Claude",
4949
showInteractionModeToggle: true,
5050
} as const;
51+
const MINIMUM_CLAUDE_OPUS_4_8_VERSION = "2.1.154";
5152
const MINIMUM_CLAUDE_OPUS_4_7_VERSION = "2.1.111";
53+
54+
const CLAUDE_EFFORT_OPTIONS = {
55+
opus48: [
56+
{ value: "low", label: "Low" },
57+
{ value: "medium", label: "Medium" },
58+
{ value: "high", label: "High", isDefault: true },
59+
{ value: "xhigh", label: "Extra High" },
60+
{ value: "max", label: "Max" },
61+
{ value: "ultracode", label: "Ultracode" },
62+
{ value: "ultrathink", label: "Ultrathink" },
63+
],
64+
opus47: [
65+
{ value: "low", label: "Low" },
66+
{ value: "medium", label: "Medium" },
67+
{ value: "high", label: "High" },
68+
{ value: "xhigh", label: "Extra High", isDefault: true },
69+
{ value: "max", label: "Max" },
70+
{ value: "ultrathink", label: "Ultrathink" },
71+
],
72+
opus46: [
73+
{ value: "low", label: "Low" },
74+
{ value: "medium", label: "Medium" },
75+
{ value: "high", label: "High", isDefault: true },
76+
{ value: "max", label: "Max" },
77+
{ value: "ultrathink", label: "Ultrathink" },
78+
],
79+
sonnet46: [
80+
{ value: "low", label: "Low" },
81+
{ value: "medium", label: "Medium" },
82+
{ value: "high", label: "High", isDefault: true },
83+
{ value: "max", label: "Max" },
84+
{ value: "ultrathink", label: "Ultrathink" },
85+
],
86+
opus45: [
87+
{ value: "low", label: "Low" },
88+
{ value: "medium", label: "Medium" },
89+
{ value: "high", label: "High", isDefault: true },
90+
{ value: "max", label: "Max" },
91+
],
92+
} as const;
93+
5294
const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
5395
{
54-
slug: "claude-opus-4-7",
55-
name: "Claude Opus 4.7",
96+
slug: "claude-opus-4-8",
97+
name: "Claude Opus 4.8",
5698
isCustom: false,
5799
capabilities: createModelCapabilities({
58100
optionDescriptors: [
59101
buildSelectOptionDescriptor({
60102
id: "effort",
61103
label: "Reasoning",
104+
options: CLAUDE_EFFORT_OPTIONS.opus48,
105+
promptInjectedValues: ["ultrathink"],
106+
}),
107+
buildSelectOptionDescriptor({
108+
id: "contextWindow",
109+
label: "Context Window",
62110
options: [
63-
{ value: "low", label: "Low" },
64-
{ value: "medium", label: "Medium" },
65-
{ value: "high", label: "High" },
66-
{ value: "xhigh", label: "Extra High", isDefault: true },
67-
{ value: "max", label: "Max" },
68-
{ value: "ultrathink", label: "Ultrathink" },
111+
{ value: "200k", label: "200k", isDefault: true },
112+
{ value: "1m", label: "1M" },
69113
],
114+
}),
115+
],
116+
}),
117+
},
118+
{
119+
slug: "claude-opus-4-7",
120+
name: "Claude Opus 4.7",
121+
isCustom: false,
122+
capabilities: createModelCapabilities({
123+
optionDescriptors: [
124+
buildSelectOptionDescriptor({
125+
id: "effort",
126+
label: "Reasoning",
127+
options: CLAUDE_EFFORT_OPTIONS.opus47,
70128
promptInjectedValues: ["ultrathink"],
71129
}),
72130
buildSelectOptionDescriptor({
@@ -89,13 +147,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
89147
buildSelectOptionDescriptor({
90148
id: "effort",
91149
label: "Reasoning",
92-
options: [
93-
{ value: "low", label: "Low" },
94-
{ value: "medium", label: "Medium" },
95-
{ value: "high", label: "High", isDefault: true },
96-
{ value: "max", label: "Max" },
97-
{ value: "ultrathink", label: "Ultrathink" },
98-
],
150+
options: CLAUDE_EFFORT_OPTIONS.opus46,
99151
promptInjectedValues: ["ultrathink"],
100152
}),
101153
buildBooleanOptionDescriptor({
@@ -122,12 +174,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
122174
buildSelectOptionDescriptor({
123175
id: "effort",
124176
label: "Reasoning",
125-
options: [
126-
{ value: "low", label: "Low" },
127-
{ value: "medium", label: "Medium" },
128-
{ value: "high", label: "High", isDefault: true },
129-
{ value: "max", label: "Max" },
130-
],
177+
options: CLAUDE_EFFORT_OPTIONS.opus45,
131178
}),
132179
buildBooleanOptionDescriptor({
133180
id: "fastMode",
@@ -145,12 +192,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
145192
buildSelectOptionDescriptor({
146193
id: "effort",
147194
label: "Reasoning",
148-
options: [
149-
{ value: "low", label: "Low" },
150-
{ value: "medium", label: "Medium" },
151-
{ value: "high", label: "High", isDefault: true },
152-
{ value: "ultrathink", label: "Ultrathink" },
153-
],
195+
options: CLAUDE_EFFORT_OPTIONS.sonnet46,
154196
promptInjectedValues: ["ultrathink"],
155197
}),
156198
buildSelectOptionDescriptor({
@@ -179,17 +221,31 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
179221
},
180222
];
181223

224+
function supportsClaudeOpus48(version: string | null | undefined): boolean {
225+
return version ? compareSemverVersions(version, MINIMUM_CLAUDE_OPUS_4_8_VERSION) >= 0 : false;
226+
}
227+
182228
function supportsClaudeOpus47(version: string | null | undefined): boolean {
183229
return version ? compareSemverVersions(version, MINIMUM_CLAUDE_OPUS_4_7_VERSION) >= 0 : false;
184230
}
185231

186232
function getBuiltInClaudeModelsForVersion(
187233
version: string | null | undefined,
188234
): ReadonlyArray<ServerProviderModel> {
189-
if (supportsClaudeOpus47(version)) {
190-
return BUILT_IN_MODELS;
191-
}
192-
return BUILT_IN_MODELS.filter((model) => model.slug !== "claude-opus-4-7");
235+
return BUILT_IN_MODELS.filter((model) => {
236+
if (model.slug === "claude-opus-4-8") {
237+
return supportsClaudeOpus48(version);
238+
}
239+
if (model.slug === "claude-opus-4-7") {
240+
return supportsClaudeOpus47(version);
241+
}
242+
return true;
243+
});
244+
}
245+
246+
function formatClaudeOpus48UpgradeMessage(version: string | null): string {
247+
const versionLabel = version ? `v${version}` : "the installed version";
248+
return `Claude Code ${versionLabel} is too old for Claude Opus 4.8. Upgrade to v${MINIMUM_CLAUDE_OPUS_4_8_VERSION} or newer to access it.`;
193249
}
194250

195251
function formatClaudeOpus47UpgradeMessage(version: string | null): string {
@@ -223,21 +279,34 @@ export function resolveClaudeEffort(
223279
* CLI's `--effort` flag.
224280
*
225281
* Mirrors the mapping used when invoking the Claude Agent SDK
226-
* ({@link getEffectiveClaudeAgentEffort} in ClaudeAdapter): the Opus 4.7
227-
* capability `"xhigh"` is rewritten to the accepted CLI value `"max"`, and
228-
* `"ultrathink"` is filtered out because it is a prompt-prefix mode rather
229-
* than a CLI-effort value. Returns `undefined` when no flag should be passed.
282+
* ({@link getEffectiveClaudeAgentEffort} in ClaudeAdapter): `ultracode` is a
283+
* Claude Code setting that pairs with `xhigh`, `ultrathink` is filtered out
284+
* because it is a prompt-prefix mode, and older model compatibility mappings
285+
* are preserved for current Claude Code behavior.
230286
*/
231-
export function normalizeClaudeCliEffort(effort: string | null | undefined): string | undefined {
287+
export function normalizeClaudeCliEffort(
288+
effort: string | null | undefined,
289+
model: string | null | undefined,
290+
): string | undefined {
232291
if (!effort || effort === "ultrathink") {
233292
return undefined;
234293
}
235-
if (effort === "xhigh") {
294+
if (effort === "ultracode") {
295+
return "xhigh";
296+
}
297+
if (effort === "xhigh" && model !== "claude-opus-4-8") {
236298
return "max";
237299
}
300+
if (effort === "max" && model === "claude-sonnet-4-6") {
301+
return "high";
302+
}
238303
return effort;
239304
}
240305

306+
export function isClaudeUltracodeEffort(effort: string | null | undefined): boolean {
307+
return effort === "ultracode";
308+
}
309+
241310
export function resolveClaudeApiModelId(modelSelection: ModelSelection): string {
242311
switch (getModelSelectionStringOptionValue(modelSelection, "contextWindow")) {
243312
case "1m":
@@ -617,9 +686,11 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
617686
claudeSettings.customModels,
618687
DEFAULT_CLAUDE_MODEL_CAPABILITIES,
619688
);
620-
const opus47UpgradeMessage = supportsClaudeOpus47(parsedVersion)
689+
const versionUpgradeMessage = supportsClaudeOpus48(parsedVersion)
621690
? undefined
622-
: formatClaudeOpus47UpgradeMessage(parsedVersion);
691+
: supportsClaudeOpus47(parsedVersion)
692+
? formatClaudeOpus48UpgradeMessage(parsedVersion)
693+
: formatClaudeOpus47UpgradeMessage(parsedVersion);
623694

624695
const capabilities = resolveCapabilities
625696
? yield* resolveCapabilities(claudeSettings).pipe(Effect.orElseSucceed(() => undefined))
@@ -663,7 +734,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
663734
...(capabilities.email ? { email: capabilities.email } : {}),
664735
...(authMetadata ? authMetadata : {}),
665736
},
666-
...(opus47UpgradeMessage ? { message: opus47UpgradeMessage } : {}),
737+
...(versionUpgradeMessage ? { message: versionUpgradeMessage } : {}),
667738
},
668739
});
669740
});

apps/server/src/textGeneration/ClaudeTextGeneration.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
} from "@t3tools/shared/model";
3838
import {
3939
getClaudeModelCapabilities,
40+
isClaudeUltracodeEffort,
4041
normalizeClaudeCliEffort,
4142
resolveClaudeApiModelId,
4243
resolveClaudeEffort,
@@ -132,7 +133,8 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu
132133
const findDescriptor = (id: string) => descriptors.find((descriptor) => descriptor.id === id);
133134
const rawEffortSelection = getModelSelectionStringOptionValue(modelSelection, "effort");
134135
const resolvedEffort = resolveClaudeEffort(caps, rawEffortSelection);
135-
const cliEffort = normalizeClaudeCliEffort(resolvedEffort);
136+
const cliEffort = normalizeClaudeCliEffort(resolvedEffort, modelSelection.model);
137+
const ultracode = isClaudeUltracodeEffort(resolvedEffort);
136138
const thinkingDescriptor = findDescriptor("thinking");
137139
const fastModeDescriptor = findDescriptor("fastMode");
138140
const thinking =
@@ -142,6 +144,7 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu
142144
const settings = {
143145
...(typeof thinking === "boolean" ? { alwaysThinkingEnabled: thinking } : {}),
144146
...(fastMode ? { fastMode: true } : {}),
147+
...(ultracode ? { ultracode: true } : {}),
145148
};
146149
const settingsJson =
147150
Object.keys(settings).length > 0

bun.lock

Lines changed: 19 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/contracts/src/model.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,9 @@ export const MODEL_SLUG_ALIASES_BY_PROVIDER: Partial<
164164
"gpt-5.3-spark": "gpt-5.3-codex-spark",
165165
},
166166
[CLAUDE_DRIVER_KIND]: {
167-
opus: "claude-opus-4-7",
167+
opus: "claude-opus-4-8",
168+
"opus-4.8": "claude-opus-4-8",
169+
"claude-opus-4.8": "claude-opus-4-8",
168170
"opus-4.7": "claude-opus-4-7",
169171
"claude-opus-4.7": "claude-opus-4-7",
170172
"opus-4.6": "claude-opus-4-6",

0 commit comments

Comments
 (0)