Skip to content

Commit c73a6d2

Browse files
committed
feat: support xhigh for Claude Opus 4.7
1 parent 2725360 commit c73a6d2

11 files changed

Lines changed: 188 additions & 32 deletions

docs/tools/thinking.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@ title: "Thinking Levels"
1515
- low → “think hard”
1616
- medium → “think harder”
1717
- high → “ultrathink” (max budget)
18-
- xhigh → “ultrathink+” (GPT-5.2 + Codex models only)
19-
- adaptive → provider-managed adaptive reasoning budget (supported for Anthropic Claude 4.6 model family)
18+
- xhigh → “ultrathink+” (GPT-5.2 + Codex models and Anthropic Claude Opus 4.7)
19+
- adaptive → provider-managed adaptive reasoning budget (supported for Anthropic Claude 4.6 and Opus 4.7)
2020
- `x-high`, `x_high`, `extra-high`, `extra high`, and `extra_high` map to `xhigh`.
2121
- `highest`, `max` map to `high`.
2222
- Provider notes:
23-
- Anthropic Claude 4.6 models default to `adaptive` when no explicit thinking level is set.
23+
- Anthropic Claude 4.6 and Opus 4.7 models default to `adaptive` when no explicit thinking level is set.
24+
- Anthropic Claude Opus 4.7 maps `/think xhigh` to `output_config.effort: "xhigh"`.
2425
- MiniMax (`minimax/*`) on the Anthropic-compatible streaming path defaults to `thinking: { type: "disabled" }` unless you explicitly set thinking in model params or request params. This avoids leaked `reasoning_content` deltas from MiniMax's non-native Anthropic stream format.
2526
- Z.AI (`zai/*`) only supports binary thinking (`on`/`off`). Any non-`off` level is treated as `on` (mapped to `low`).
2627
- Moonshot (`moonshot/*`) maps `/think off` to `thinking: { type: "disabled" }` and any non-`off` level to `thinking: { type: "enabled" }`. When thinking is enabled, Moonshot only accepts `tool_choice` `auto|none`; OpenClaw normalizes incompatible values to `auto`.
@@ -31,7 +32,7 @@ title: "Thinking Levels"
3132
2. Session override (set by sending a directive-only message).
3233
3. Per-agent default (`agents.list[].thinkingDefault` in config).
3334
4. Global default (`agents.defaults.thinkingDefault` in config).
34-
5. Fallback: `adaptive` for Anthropic Claude 4.6 models, `low` for other reasoning-capable models, `off` otherwise.
35+
5. Fallback: `adaptive` for Anthropic Claude 4.6 and Opus 4.7 models, `low` for other reasoning-capable models, `off` otherwise.
3536

3637
## Setting a session default
3738

@@ -104,8 +105,9 @@ title: "Thinking Levels"
104105

105106
- The web chat thinking selector mirrors the session's stored level from the inbound session store/config when the page loads.
106107
- Picking another level writes the session override immediately via `sessions.patch`; it does not wait for the next send and it is not a one-shot `thinkingOnce` override.
107-
- The first option is always `Default (<resolved level>)`, where the resolved default comes from the active session model: `adaptive` for Claude 4.6 on Anthropic/Bedrock, `low` for other reasoning-capable models, `off` otherwise.
108+
- The first option is always `Default (<resolved level>)`, where the resolved default comes from the active session model: `adaptive` for Claude 4.6 and Opus 4.7 on Anthropic, `low` for other reasoning-capable models, `off` otherwise.
108109
- The picker stays provider-aware:
109110
- most providers show `off | minimal | low | medium | high | adaptive`
111+
- Anthropic Claude Opus 4.7 shows `off | minimal | low | medium | high | xhigh | adaptive`
110112
- Z.AI shows binary `off | on`
111113
- `/think:<level>` still works and updates the same stored session level, so chat directives and the picker stay in sync.

extensions/anthropic/index.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,18 @@ describe("anthropic provider replay hooks", () => {
200200
modelId: "claude-opus-4-7",
201201
} as never),
202202
).toBe("adaptive");
203+
expect(
204+
provider.supportsXHighThinking?.({
205+
provider: "anthropic",
206+
modelId: "claude-opus-4-7",
207+
} as never),
208+
).toBe(true);
209+
expect(
210+
provider.supportsXHighThinking?.({
211+
provider: "anthropic",
212+
modelId: "claude-opus-4-6",
213+
} as never),
214+
).toBe(false);
203215
});
204216

205217
it("resolves claude-cli synthetic oauth auth", async () => {

extensions/anthropic/register.runtime.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,15 +260,22 @@ function resolveAnthropicForwardCompatModel(
260260
function shouldUseAnthropicAdaptiveThinkingDefault(modelId: string): boolean {
261261
const lowerModelId = normalizeLowercaseStringOrEmpty(modelId);
262262
return (
263-
lowerModelId.startsWith(ANTHROPIC_OPUS_47_MODEL_ID) ||
264-
lowerModelId.startsWith(ANTHROPIC_OPUS_47_DOT_MODEL_ID) ||
263+
isAnthropicOpus47Model(lowerModelId) ||
265264
lowerModelId.startsWith(ANTHROPIC_OPUS_46_MODEL_ID) ||
266265
lowerModelId.startsWith(ANTHROPIC_OPUS_46_DOT_MODEL_ID) ||
267266
lowerModelId.startsWith(ANTHROPIC_SONNET_46_MODEL_ID) ||
268267
lowerModelId.startsWith(ANTHROPIC_SONNET_46_DOT_MODEL_ID)
269268
);
270269
}
271270

271+
function isAnthropicOpus47Model(modelId: string): boolean {
272+
const lowerModelId = normalizeLowercaseStringOrEmpty(modelId);
273+
return (
274+
lowerModelId.startsWith(ANTHROPIC_OPUS_47_MODEL_ID) ||
275+
lowerModelId.startsWith(ANTHROPIC_OPUS_47_DOT_MODEL_ID)
276+
);
277+
}
278+
272279
function matchesAnthropicModernModel(modelId: string): boolean {
273280
const lower = normalizeLowercaseStringOrEmpty(modelId);
274281
return ANTHROPIC_MODERN_MODEL_PREFIXES.some((prefix) => lower.startsWith(prefix));
@@ -481,6 +488,7 @@ export function registerAnthropicPlugin(api: OpenClawPluginApi): void {
481488
buildReplayPolicy: buildAnthropicReplayPolicy,
482489
isModernModelRef: ({ modelId }) => matchesAnthropicModernModel(modelId),
483490
resolveReasoningOutputMode: () => "native",
491+
supportsXHighThinking: ({ modelId }) => isAnthropicOpus47Model(modelId),
484492
wrapStreamFn: wrapAnthropicProviderStream,
485493
resolveDefaultThinkingLevel: ({ modelId }) =>
486494
matchesAnthropicModernModel(modelId) && shouldUseAnthropicAdaptiveThinkingDefault(modelId)

src/agents/anthropic-transport-stream.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,4 +471,49 @@ describe("anthropic transport stream", () => {
471471
undefined,
472472
);
473473
});
474+
475+
it("maps xhigh thinking effort for Claude Opus 4.7 transport runs", async () => {
476+
const model = attachModelProviderRequestTransport(
477+
{
478+
id: "claude-opus-4-7",
479+
name: "Claude Opus 4.7",
480+
api: "anthropic-messages",
481+
provider: "anthropic",
482+
baseUrl: "https://api.anthropic.com",
483+
reasoning: true,
484+
input: ["text"],
485+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
486+
contextWindow: 200000,
487+
maxTokens: 8192,
488+
} satisfies Model<"anthropic-messages">,
489+
{
490+
proxy: {
491+
mode: "env-proxy",
492+
},
493+
},
494+
);
495+
const streamFn = createAnthropicMessagesTransportStreamFn();
496+
497+
const stream = await Promise.resolve(
498+
streamFn(
499+
model,
500+
{
501+
messages: [{ role: "user", content: "Think extra hard." }],
502+
} as Parameters<typeof streamFn>[1],
503+
{
504+
apiKey: "sk-ant-api",
505+
reasoning: "xhigh",
506+
} as Parameters<typeof streamFn>[2],
507+
),
508+
);
509+
await stream.result();
510+
511+
expect(anthropicMessagesStreamMock).toHaveBeenCalledWith(
512+
expect.objectContaining({
513+
thinking: { type: "adaptive" },
514+
output_config: { effort: "xhigh" },
515+
}),
516+
undefined,
517+
);
518+
});
474519
});

src/agents/anthropic-transport-stream.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ type AnthropicTransportModel = Model<"anthropic-messages"> & {
5959

6060
type AnthropicTransportOptions = AnthropicOptions &
6161
Pick<SimpleStreamOptions, "reasoning" | "thinkingBudgets">;
62+
type AnthropicAdaptiveEffort = NonNullable<AnthropicOptions["effort"]> | "xhigh";
6263

6364
type TransportContentBlock =
6465
| { type: "text"; text: string; index?: number }
@@ -98,27 +99,35 @@ type MutableAssistantOutput = {
9899
errorMessage?: string;
99100
};
100101

102+
function isClaudeOpus47Model(modelId: string): boolean {
103+
return modelId.includes("opus-4-7") || modelId.includes("opus-4.7");
104+
}
105+
106+
function isClaudeOpus46Model(modelId: string): boolean {
107+
return modelId.includes("opus-4-6") || modelId.includes("opus-4.6");
108+
}
109+
101110
function supportsAdaptiveThinking(modelId: string): boolean {
102111
return (
103-
modelId.includes("opus-4-6") ||
104-
modelId.includes("opus-4.6") ||
112+
isClaudeOpus47Model(modelId) ||
113+
isClaudeOpus46Model(modelId) ||
105114
modelId.includes("sonnet-4-6") ||
106115
modelId.includes("sonnet-4.6")
107116
);
108117
}
109118

110-
function mapThinkingLevelToEffort(
111-
level: ThinkingLevel,
112-
modelId: string,
113-
): NonNullable<AnthropicOptions["effort"]> {
119+
function mapThinkingLevelToEffort(level: ThinkingLevel, modelId: string): AnthropicAdaptiveEffort {
114120
switch (level) {
115121
case "minimal":
116122
case "low":
117123
return "low";
118124
case "medium":
119125
return "medium";
120126
case "xhigh":
121-
return modelId.includes("opus-4-6") || modelId.includes("opus-4.6") ? "max" : "high";
127+
if (isClaudeOpus47Model(modelId)) {
128+
return "xhigh";
129+
}
130+
return isClaudeOpus46Model(modelId) ? "max" : "high";
122131
default:
123132
return "high";
124133
}
@@ -616,7 +625,9 @@ function resolveAnthropicTransportOptions(
616625
}
617626
if (supportsAdaptiveThinking(model.id)) {
618627
resolved.thinkingEnabled = true;
619-
resolved.effort = mapThinkingLevelToEffort(options.reasoning, model.id);
628+
resolved.effort = mapThinkingLevelToEffort(options.reasoning, model.id) as NonNullable<
629+
AnthropicOptions["effort"]
630+
>;
620631
return resolved;
621632
}
622633
const adjusted = adjustMaxTokensForThinking({

src/agents/anthropic-vertex-stream.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,22 @@ describe("createAnthropicVertexStreamFn", () => {
146146
);
147147
});
148148

149+
it("maps xhigh reasoning to xhigh effort for Opus 4.7", () => {
150+
const streamFn = createAnthropicVertexStreamFn("vertex-project", "us-east5");
151+
const model = makeModel({ id: "claude-opus-4-7", maxTokens: 64000 });
152+
153+
void streamFn(model, { messages: [] }, { reasoning: "xhigh" });
154+
155+
expect(hoisted.streamAnthropicMock).toHaveBeenCalledWith(
156+
model,
157+
{ messages: [] },
158+
expect.objectContaining({
159+
thinkingEnabled: true,
160+
effort: "xhigh",
161+
}),
162+
);
163+
});
164+
149165
it("applies Anthropic cache-boundary shaping before forwarding payload hooks", async () => {
150166
const streamFn = createAnthropicVertexStreamFn("vertex-project", "us-east5");
151167
const model = makeModel({ id: "claude-sonnet-4-6", maxTokens: 64000 });

src/agents/anthropic-vertex-stream.ts

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,38 @@ import {
1111
} from "./anthropic-payload-policy.js";
1212

1313
type AnthropicVertexEffort = NonNullable<AnthropicOptions["effort"]>;
14+
type AnthropicVertexAdaptiveEffort = AnthropicVertexEffort | "xhigh";
15+
16+
function isClaudeOpus47Model(modelId: string): boolean {
17+
return modelId.includes("opus-4-7") || modelId.includes("opus-4.7");
18+
}
19+
20+
function isClaudeOpus46Model(modelId: string): boolean {
21+
return modelId.includes("opus-4-6") || modelId.includes("opus-4.6");
22+
}
23+
24+
function supportsAdaptiveThinking(modelId: string): boolean {
25+
return (
26+
isClaudeOpus47Model(modelId) ||
27+
isClaudeOpus46Model(modelId) ||
28+
modelId.includes("sonnet-4-6") ||
29+
modelId.includes("sonnet-4.6")
30+
);
31+
}
32+
33+
function mapAnthropicAdaptiveEffort(
34+
reasoning: string,
35+
modelId: string,
36+
): AnthropicVertexAdaptiveEffort {
37+
const effortMap: Record<string, AnthropicVertexAdaptiveEffort> = {
38+
minimal: "low",
39+
low: "low",
40+
medium: "medium",
41+
high: "high",
42+
xhigh: isClaudeOpus47Model(modelId) ? "xhigh" : isClaudeOpus46Model(modelId) ? "max" : "high",
43+
};
44+
return effortMap[reasoning] ?? "high";
45+
}
1446

1547
function resolveAnthropicVertexMaxTokens(params: {
1648
modelMaxTokens: number | undefined;
@@ -110,22 +142,12 @@ export function createAnthropicVertexStreamFn(
110142
};
111143

112144
if (options?.reasoning) {
113-
const isAdaptive =
114-
model.id.includes("opus-4-6") ||
115-
model.id.includes("opus-4.6") ||
116-
model.id.includes("sonnet-4-6") ||
117-
model.id.includes("sonnet-4.6");
118-
119-
if (isAdaptive) {
145+
if (supportsAdaptiveThinking(model.id)) {
120146
opts.thinkingEnabled = true;
121-
const effortMap: Record<string, AnthropicVertexEffort> = {
122-
minimal: "low",
123-
low: "low",
124-
medium: "medium",
125-
high: "high",
126-
xhigh: model.id.includes("opus-4-6") || model.id.includes("opus-4.6") ? "max" : "high",
127-
};
128-
opts.effort = effortMap[options.reasoning] ?? "high";
147+
opts.effort = mapAnthropicAdaptiveEffort(
148+
options.reasoning,
149+
model.id,
150+
) as AnthropicVertexEffort;
129151
} else {
130152
opts.thinkingEnabled = true;
131153
const budgets = options.thinkingBudgets;

src/agents/model-selection.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ const ANTHROPIC_OPUS_CATALOG = [
4848
},
4949
];
5050

51+
const ANTHROPIC_OPUS_47_CATALOG = [
52+
{
53+
provider: "anthropic",
54+
id: "claude-opus-4-7",
55+
name: "Claude Opus 4.7",
56+
reasoning: true,
57+
},
58+
];
59+
5160
function resolveAnthropicOpusThinking(cfg: OpenClawConfig) {
5261
return resolveThinkingDefault({
5362
cfg,
@@ -57,6 +66,15 @@ function resolveAnthropicOpusThinking(cfg: OpenClawConfig) {
5766
});
5867
}
5968

69+
function resolveAnthropicOpus47Thinking(cfg: OpenClawConfig) {
70+
return resolveThinkingDefault({
71+
cfg,
72+
provider: "anthropic",
73+
model: "claude-opus-4-7",
74+
catalog: ANTHROPIC_OPUS_47_CATALOG,
75+
});
76+
}
77+
6078
function createAgentFallbackConfig(params: {
6179
primary?: string;
6280
fallbacks?: string[];
@@ -1158,6 +1176,18 @@ describe("model-selection", () => {
11581176
expect(resolveAnthropicOpusThinking(cfg)).toBe("adaptive");
11591177
});
11601178

1179+
it("uses adaptive fallback for explicitly configured Anthropic Opus 4.7", () => {
1180+
const cfg = {
1181+
agents: {
1182+
defaults: {
1183+
model: { primary: "anthropic/claude-opus-4-7" },
1184+
},
1185+
},
1186+
} as OpenClawConfig;
1187+
1188+
expect(resolveAnthropicOpus47Thinking(cfg)).toBe("adaptive");
1189+
});
1190+
11611191
it("falls back to low when no provider thinking hook is active", () => {
11621192
const cfg = {} as OpenClawConfig;
11631193

src/agents/model-thinking-default.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,9 @@ export function resolveThinkingDefault(params: {
5858
normalizedProvider === "anthropic" &&
5959
explicitModelConfigured &&
6060
typeof catalogCandidate?.name === "string" &&
61-
/4\.6\b/.test(catalogCandidate.name) &&
62-
(normalizedModel.startsWith("claude-opus-4-6") ||
61+
/4\.[67]\b/.test(catalogCandidate.name) &&
62+
(normalizedModel.startsWith("claude-opus-4-7") ||
63+
normalizedModel.startsWith("claude-opus-4-6") ||
6364
normalizedModel.startsWith("claude-sonnet-4-6"))
6465
) {
6566
return "adaptive";

src/auto-reply/thinking.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ describe("listThinkingLevels", () => {
8181
expect(listThinkingLevels("demo", "demo-model")).toContain("xhigh");
8282
});
8383

84+
it("uses provider runtime hooks for xhigh labels", () => {
85+
providerRuntimeMocks.resolveProviderXHighThinking.mockReturnValue(true);
86+
87+
expect(listThinkingLevelLabels("demo", "demo-model")).toContain("xhigh");
88+
});
89+
8490
it("includes xhigh for provider-advertised models", () => {
8591
providerRuntimeMocks.resolveProviderXHighThinking.mockImplementation(({ provider, context }) =>
8692
(provider === "openai" && ["gpt-5.4", "gpt-5.4", "gpt-5.4-pro"].includes(context.modelId)) ||

0 commit comments

Comments
 (0)