Skip to content

Commit 9c174fb

Browse files
committed
Support xhigh effort for Anthropic adaptive thinking (Opus 4.7+)
Claude Opus 4.7 supports the 'xhigh' effort level in adaptive thinking. Extend the effort package to accept xhigh for Anthropic, allow 'adaptive/xhigh' as a thinking_budget value, and update the JSON schema accordingly. Also simplify the effort package internals while touching it: unify allLevels and adaptiveEfforts into consistent map[Level]bool sets, replace the linear loop in Parse with a direct map lookup, and extract the repeated lowercase/trim into a small normalize helper. Assisted-By: docker-agent
1 parent dd15131 commit 9c174fb

4 files changed

Lines changed: 34 additions & 27 deletions

File tree

agent-schema.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -615,12 +615,12 @@
615615
"description": "Whether to track usage"
616616
},
617617
"thinking_budget": {
618-
"description": "Controls reasoning effort/budget. Use 'none' or 0 to disable thinking. OpenAI: string levels ('minimal','low','medium','high','xhigh'). Anthropic: integer token budget (1024-32768), 'adaptive' (adaptive thinking with high effort by default), 'adaptive/<effort>' where effort is low/medium/high/max (adaptive thinking with specified effort), or effort levels ('low','medium','high','max') which use adaptive thinking with the given effort. Amazon Bedrock (Claude): integer token budget or effort levels ('low','medium','high') mapped to token budgets. Google Gemini 2.5: integer token budget (-1 for dynamic, 0 to disable, 24576 max). Google Gemini 3: string levels ('minimal' Flash only,'low','medium','high'). Thinking is only enabled when explicitly configured.",
618+
"description": "Controls reasoning effort/budget. Use 'none' or 0 to disable thinking. OpenAI: string levels ('minimal','low','medium','high','xhigh'). Anthropic: integer token budget (1024-32768), 'adaptive' (adaptive thinking with high effort by default), 'adaptive/<effort>' where effort is low/medium/high/xhigh/max ('xhigh' is supported by Claude Opus 4.7+), or effort levels ('low','medium','high','xhigh','max') which use adaptive thinking with the given effort. Amazon Bedrock (Claude): integer token budget or effort levels ('low','medium','high') mapped to token budgets. Google Gemini 2.5: integer token budget (-1 for dynamic, 0 to disable, 24576 max). Google Gemini 3: string levels ('minimal' Flash only,'low','medium','high'). Thinking is only enabled when explicitly configured.",
619619
"oneOf": [
620620
{
621621
"type": "string",
622-
"pattern": "^(none|minimal|low|medium|high|xhigh|max|adaptive(/low|/medium|/high|/max)?)$",
623-
"description": "Reasoning effort level. 'adaptive' uses adaptive thinking with high effort (default). 'adaptive/<effort>' specifies the effort level (low/medium/high/max). Use 'none' to disable thinking."
622+
"pattern": "^(none|minimal|low|medium|high|xhigh|max|adaptive(/low|/medium|/high|/xhigh|/max)?)$",
623+
"description": "Reasoning effort level. 'adaptive' uses adaptive thinking with high effort (default). 'adaptive/<effort>' specifies the effort level (low/medium/high/xhigh/max). 'adaptive/xhigh' requires Claude Opus 4.7+. Use 'none' to disable thinking."
624624
},
625625
{
626626
"type": "integer",
@@ -642,6 +642,7 @@
642642
"adaptive/low",
643643
"adaptive/medium",
644644
"adaptive/high",
645+
"adaptive/xhigh",
645646
"adaptive/max",
646647
-1,
647648
1024,

pkg/config/latest/types_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ func TestThinkingBudget_UnmarshalYAML_AdaptiveWithEffort(t *testing.T) {
179179
{"adaptive/low", `thinking_budget: adaptive/low`, "adaptive/low"},
180180
{"adaptive/medium", `thinking_budget: adaptive/medium`, "adaptive/medium"},
181181
{"adaptive/high", `thinking_budget: adaptive/high`, "adaptive/high"},
182+
{"adaptive/xhigh", `thinking_budget: adaptive/xhigh`, "adaptive/xhigh"},
182183
{"adaptive/max", `thinking_budget: adaptive/max`, "adaptive/max"},
183184
} {
184185
t.Run(tt.name, func(t *testing.T) {
@@ -221,6 +222,7 @@ func TestThinkingBudget_IsAdaptive(t *testing.T) {
221222
{"adaptive/high", &ThinkingBudget{Effort: "adaptive/high"}, true},
222223
{"adaptive/low", &ThinkingBudget{Effort: "adaptive/low"}, true},
223224
{"adaptive/medium", &ThinkingBudget{Effort: "adaptive/medium"}, true},
225+
{"adaptive/xhigh", &ThinkingBudget{Effort: "adaptive/xhigh"}, true},
224226
{"adaptive/max", &ThinkingBudget{Effort: "adaptive/max"}, true},
225227
{"medium", &ThinkingBudget{Effort: "medium"}, false},
226228
{"tokens", &ThinkingBudget{Tokens: 8192}, false},
@@ -246,6 +248,7 @@ func TestThinkingBudget_AdaptiveEffort(t *testing.T) {
246248
{"adaptive/low", &ThinkingBudget{Effort: "adaptive/low"}, "low", true},
247249
{"adaptive/medium", &ThinkingBudget{Effort: "adaptive/medium"}, "medium", true},
248250
{"adaptive/high", &ThinkingBudget{Effort: "adaptive/high"}, "high", true},
251+
{"adaptive/xhigh", &ThinkingBudget{Effort: "adaptive/xhigh"}, "xhigh", true},
249252
{"adaptive/max", &ThinkingBudget{Effort: "adaptive/max"}, "max", true},
250253
{"not adaptive", &ThinkingBudget{Effort: "medium"}, "", false},
251254
{"tokens", &ThinkingBudget{Tokens: 8192}, "", false},

pkg/effort/effort.go

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,23 +23,28 @@ const (
2323
Max Level = "max"
2424
)
2525

26-
// allLevels lists every non-adaptive level in ascending order.
27-
var allLevels = []Level{None, Minimal, Low, Medium, High, XHigh, Max}
26+
// allLevels is the set of recognised non-adaptive effort levels.
27+
var allLevels = map[Level]bool{
28+
None: true, Minimal: true, Low: true, Medium: true, High: true, XHigh: true, Max: true,
29+
}
2830

2931
// adaptiveEfforts are the effort sub-levels valid after "adaptive/".
30-
var adaptiveEfforts = map[string]bool{
31-
string(Low): true, string(Medium): true, string(High): true, string(Max): true,
32+
var adaptiveEfforts = map[Level]bool{
33+
Low: true, Medium: true, High: true, XHigh: true, Max: true,
34+
}
35+
36+
// normalize lowercases and trims s for case-insensitive matching.
37+
func normalize(s string) Level {
38+
return Level(strings.ToLower(strings.TrimSpace(s)))
3239
}
3340

3441
// Parse normalises s (case-insensitive, trimmed) and returns the matching
3542
// Level. It returns ("", false) for unknown strings, adaptive values, and
3643
// empty input. Use [IsValid] for full validation including adaptive forms.
3744
func Parse(s string) (Level, bool) {
38-
norm := strings.ToLower(strings.TrimSpace(s))
39-
for _, l := range allLevels {
40-
if norm == string(l) {
41-
return l, true
42-
}
45+
l := normalize(s)
46+
if allLevels[l] {
47+
return l, true
4348
}
4449
return "", false
4550
}
@@ -48,22 +53,19 @@ func Parse(s string) (Level, bool) {
4853
// It accepts every [Level] constant, plain "adaptive", and the
4954
// "adaptive/<effort>" form.
5055
func IsValid(s string) bool {
51-
if _, ok := Parse(s); ok {
52-
return true
53-
}
54-
norm := strings.ToLower(strings.TrimSpace(s))
55-
if norm == "adaptive" {
56+
norm := normalize(s)
57+
if allLevels[norm] || norm == "adaptive" {
5658
return true
5759
}
58-
if after, ok := strings.CutPrefix(norm, "adaptive/"); ok {
59-
return adaptiveEfforts[after]
60+
if after, ok := strings.CutPrefix(string(norm), "adaptive/"); ok {
61+
return adaptiveEfforts[Level(after)]
6062
}
6163
return false
6264
}
6365

6466
// IsValidAdaptive reports whether sub is a valid effort for "adaptive/<sub>".
6567
func IsValidAdaptive(sub string) bool {
66-
return adaptiveEfforts[strings.ToLower(strings.TrimSpace(sub))]
68+
return adaptiveEfforts[normalize(sub)]
6769
}
6870

6971
// ValidNames returns a human-readable list of accepted values, suitable for
@@ -88,13 +90,14 @@ func ForOpenAI(l Level) (string, bool) {
8890
}
8991

9092
// ForAnthropic returns the Anthropic output_config effort string for l.
91-
// Anthropic accepts: low, medium, high, max.
93+
// Anthropic accepts: low, medium, high, xhigh, max.
94+
// xhigh is only supported by newer Claude models (e.g. Opus 4.7+).
9295
// Minimal is mapped to low as the closest equivalent.
9396
func ForAnthropic(l Level) (string, bool) {
9497
switch l {
9598
case Minimal:
9699
return string(Low), true
97-
case Low, Medium, High, Max:
100+
case Low, Medium, High, XHigh, Max:
98101
return string(l), true
99102
default:
100103
return "", false

pkg/effort/effort_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func TestIsValid(t *testing.T) {
4343

4444
valid := []string{
4545
"none", "minimal", "low", "medium", "high", "xhigh", "max",
46-
"adaptive", "adaptive/low", "adaptive/medium", "adaptive/high", "adaptive/max",
46+
"adaptive", "adaptive/low", "adaptive/medium", "adaptive/high", "adaptive/xhigh", "adaptive/max",
4747
"ADAPTIVE/HIGH", " adaptive ",
4848
}
4949
for _, s := range valid {
@@ -54,7 +54,7 @@ func TestIsValid(t *testing.T) {
5454
}
5555

5656
invalid := []string{
57-
"", "unknown", "adaptive/none", "adaptive/minimal", "adaptive/xhigh",
57+
"", "unknown", "adaptive/none", "adaptive/minimal",
5858
"adaptive/", "adaptive/foo",
5959
}
6060
for _, s := range invalid {
@@ -102,8 +102,8 @@ func TestForAnthropic(t *testing.T) {
102102
{Low, "low", true},
103103
{Medium, "medium", true},
104104
{High, "high", true},
105+
{XHigh, "xhigh", true},
105106
{Max, "max", true},
106-
{XHigh, "", false},
107107
{None, "", false},
108108
} {
109109
t.Run(string(tt.level), func(t *testing.T) {
@@ -168,15 +168,15 @@ func TestForGemini3(t *testing.T) {
168168
func TestIsValidAdaptive(t *testing.T) {
169169
t.Parallel()
170170

171-
valid := []string{"low", "medium", "high", "max", "HIGH", " Medium "}
171+
valid := []string{"low", "medium", "high", "xhigh", "max", "HIGH", " Medium "}
172172
for _, s := range valid {
173173
t.Run("valid_"+s, func(t *testing.T) {
174174
t.Parallel()
175175
assert.True(t, IsValidAdaptive(s), "expected %q to be valid", s)
176176
})
177177
}
178178

179-
invalid := []string{"", "none", "minimal", "xhigh", "unknown", "adaptive", "adaptive/high"}
179+
invalid := []string{"", "none", "minimal", "unknown", "adaptive", "adaptive/high"}
180180
for _, s := range invalid {
181181
t.Run("invalid_"+s, func(t *testing.T) {
182182
t.Parallel()

0 commit comments

Comments
 (0)