@@ -58,6 +58,122 @@ func requiresAdaptiveThinking(model string) bool {
5858 return defaultsToAdaptiveThinking (model ) || defaultsToOmittedOpusThinkingDisplay (model )
5959}
6060
61+ // Claude models before 4.6 reject adaptive thinking. Unknown versions use
62+ // legacy budget thinking unless explicitly allowlisted.
63+ func supportsAdaptiveThinking (model string ) bool {
64+ if defaultsToAdaptiveThinking (model ) {
65+ return true
66+ }
67+ major , minor , ok := claudeVersion (model )
68+ if ! ok {
69+ return false
70+ }
71+ return major > 4 || (major == 4 && minor >= 6 )
72+ }
73+
74+ func claudeVersion (model string ) (major , minor int , ok bool ) {
75+ model = strings .ToLower (strings .TrimSpace (model ))
76+ _ , rest , found := strings .Cut (model , "claude-" )
77+ if ! found {
78+ return 0 , 0 , false
79+ }
80+ parts := strings .Split (rest , "-" )
81+ for i , part := range parts {
82+ v , valid := shortVersionComponent (part )
83+ if ! valid {
84+ continue
85+ }
86+ major = v
87+ if i + 1 < len (parts ) {
88+ if m , validMinor := shortVersionComponent (parts [i + 1 ]); validMinor {
89+ minor = m
90+ }
91+ }
92+ return major , minor , true
93+ }
94+ return 0 , 0 , false
95+ }
96+
97+ func shortVersionComponent (s string ) (int , bool ) {
98+ if len (s ) == 0 || len (s ) > 2 {
99+ return 0 , false
100+ }
101+ v , err := strconv .Atoi (s )
102+ if err != nil {
103+ return 0 , false
104+ }
105+ return v , true
106+ }
107+
108+ // Anthropic accepts only low, medium, high, or max for output_config.effort.
109+ func normalizeEffort (effort Effort ) Effort {
110+ switch effort {
111+ case EffortMinimal :
112+ return EffortLow
113+ case EffortXHigh :
114+ return EffortMax
115+ default :
116+ return effort
117+ }
118+ }
119+
120+ // Anthropic requires at least 1024 thinking tokens. Smaller derived budgets
121+ // disable thinking to preserve the requested output limit.
122+ func legacyEffortBudget (effort Effort , maxTokens int64 ) int64 {
123+ const minBudget = 1024
124+ var budget int64
125+ switch effort {
126+ case EffortMinimal :
127+ budget = minBudget
128+ case EffortLow :
129+ budget = int64 (float64 (maxTokens ) * 0.2 )
130+ case EffortHigh :
131+ budget = int64 (float64 (maxTokens ) * 0.8 )
132+ case EffortXHigh :
133+ budget = int64 (float64 (maxTokens ) * 0.9 )
134+ case EffortMax :
135+ budget = int64 (float64 (maxTokens ) * 0.95 )
136+ default :
137+ budget = int64 (float64 (maxTokens ) * 0.5 )
138+ }
139+ // budget_tokens must be strictly less than max_tokens.
140+ if budget >= maxTokens {
141+ budget = maxTokens - 1
142+ }
143+ if budget < minBudget {
144+ return 0
145+ }
146+ return budget
147+ }
148+
149+ func stripThinkingUnsupportedParams (params * anthropic.MessageNewParams , call fantasy.Call , warnings []fantasy.CallWarning ) []fantasy.CallWarning {
150+ if call .Temperature != nil {
151+ params .Temperature = param.Opt [float64 ]{}
152+ warnings = append (warnings , fantasy.CallWarning {
153+ Type : fantasy .CallWarningTypeUnsupportedSetting ,
154+ Setting : "temperature" ,
155+ Details : "temperature is not supported when thinking is enabled" ,
156+ })
157+ }
158+ if call .TopP != nil {
159+ params .TopP = param.Opt [float64 ]{}
160+ warnings = append (warnings , fantasy.CallWarning {
161+ Type : fantasy .CallWarningTypeUnsupportedSetting ,
162+ Setting : "TopP" ,
163+ Details : "TopP is not supported when thinking is enabled" ,
164+ })
165+ }
166+ if call .TopK != nil {
167+ params .TopK = param.Opt [int64 ]{}
168+ warnings = append (warnings , fantasy.CallWarning {
169+ Type : fantasy .CallWarningTypeUnsupportedSetting ,
170+ Setting : "TopK" ,
171+ Details : "TopK is not supported when thinking is enabled" ,
172+ })
173+ }
174+ return warnings
175+ }
176+
61177func setThinkingDisplay (param interface { SetExtraFields (map [string ]any ) }, display ThinkingDisplay ) {
62178 param .SetExtraFields (map [string ]any {"display" : string (display )})
63179}
@@ -392,14 +508,27 @@ func (a languageModel) prepareParams(call fantasy.Call) (
392508 switch {
393509 case providerOptions .Effort != nil :
394510 effort := * providerOptions .Effort
395- params .OutputConfig = anthropic.OutputConfigParam {
396- Effort : anthropic .OutputConfigEffort (effort ),
397- }
398- adaptive := anthropic .NewThinkingConfigAdaptiveParam ()
399- if display , ok := thinkingDisplay (providerOptions , a .modelID ); ok {
400- setThinkingDisplay (& adaptive , display )
511+ switch {
512+ case effort == EffortNone :
513+ // An explicit none overrides models that default to adaptive thinking.
514+ case supportsAdaptiveThinking (a .modelID ):
515+ params .OutputConfig = anthropic.OutputConfigParam {
516+ Effort : anthropic .OutputConfigEffort (normalizeEffort (effort )),
517+ }
518+ adaptive := anthropic .NewThinkingConfigAdaptiveParam ()
519+ if display , ok := thinkingDisplay (providerOptions , a .modelID ); ok {
520+ setThinkingDisplay (& adaptive , display )
521+ }
522+ params .Thinking .OfAdaptive = & adaptive
523+ default :
524+ if budget := legacyEffortBudget (effort , params .MaxTokens ); budget > 0 {
525+ params .Thinking = anthropic .ThinkingConfigParamOfEnabled (budget )
526+ if display , ok := thinkingDisplay (providerOptions , a .modelID ); ok {
527+ setThinkingDisplay (params .Thinking .OfEnabled , display )
528+ }
529+ warnings = stripThinkingUnsupportedParams (params , call , warnings )
530+ }
401531 }
402- params .Thinking .OfAdaptive = & adaptive
403532 case providerOptions .Thinking != nil :
404533 if providerOptions .Thinking .BudgetTokens == 0 {
405534 return nil , nil , nil , nil , & fantasy.Error {Title : "no budget" , Message : "thinking requires budget" }
@@ -416,30 +545,7 @@ func (a languageModel) prepareParams(call fantasy.Call) (
416545 setThinkingDisplay (params .Thinking .OfEnabled , display )
417546 }
418547 }
419- if call .Temperature != nil {
420- params .Temperature = param.Opt [float64 ]{}
421- warnings = append (warnings , fantasy.CallWarning {
422- Type : fantasy .CallWarningTypeUnsupportedSetting ,
423- Setting : "temperature" ,
424- Details : "temperature is not supported when thinking is enabled" ,
425- })
426- }
427- if call .TopP != nil {
428- params .TopP = param.Opt [float64 ]{}
429- warnings = append (warnings , fantasy.CallWarning {
430- Type : fantasy .CallWarningTypeUnsupportedSetting ,
431- Setting : "TopP" ,
432- Details : "TopP is not supported when thinking is enabled" ,
433- })
434- }
435- if call .TopK != nil {
436- params .TopK = param.Opt [int64 ]{}
437- warnings = append (warnings , fantasy.CallWarning {
438- Type : fantasy .CallWarningTypeUnsupportedSetting ,
439- Setting : "TopK" ,
440- Details : "TopK is not supported when thinking is enabled" ,
441- })
442- }
548+ warnings = stripThinkingUnsupportedParams (params , call , warnings )
443549 case defaultsToAdaptiveThinking (a .modelID ):
444550 adaptive := anthropic .NewThinkingConfigAdaptiveParam ()
445551 if display , ok := thinkingDisplay (providerOptions , a .modelID ); ok {
0 commit comments