feat(provider): adaptive thinking + xhigh effort for Opus 4.7/4.8
Opus 4.7+ only support adaptive thinking: explicit thinking budgets
(thinking:{type:enabled,budget_tokens:N}) and non-default sampling
params return 400. The Anthropic client now sends thinking:{type:
adaptive} plus output_config.effort and omits temperature for these
models, while older models keep the budget-based path. Adaptive models
are detected via a new Model.AdaptiveThinking flag with an id-substring
fallback so the same family reached through an Anthropic-Messages proxy
is handled too.
For adaptive Anthropic models served over the OpenAI-compatible chat-
completions wire (openrouter, opencode, ...), reasoning_effort now maps
maximum -> xhigh instead of clamping to high, preserving the model's
full reasoning ceiling. Adds AnthropicAdaptiveEffort and
OpenAICompatAnthropicEffort with tests.- commit
- 124d679982dab4fa8e8aa8f9ecd3c708e31c5932
- author
- patriceckhart · · 2026-05-29 12:21 UTC
- parent
- 872a312
- signature
- unsigned
6 files · +235 -37
M packages/provider/anthropic.go +73 -26
| @@ -162,19 +162,49 @@ type anthTool struct { | |||
| 162 | 162 | } | |
| 163 | 163 | ||
| 164 | 164 | type anthThinking struct { | |
| 165 | - | Type string `json:"type"` // "enabled" | |
| 166 | - | BudgetTokens int `json:"budget_tokens"` | |
| 165 | + | // Type is "enabled" for budget-based thinking (Opus 4.6 and | |
| 166 | + | // earlier) or "adaptive" for adaptive-thinking models (Opus 4.7+). | |
| 167 | + | Type string `json:"type"` | |
| 168 | + | // BudgetTokens is only sent for Type=="enabled". Adaptive models | |
| 169 | + | // reject it (400), so it is omitted for them. | |
| 170 | + | BudgetTokens int `json:"budget_tokens,omitempty"` | |
| 171 | + | } | |
| 172 | + | ||
| 173 | + | // anthOutputConfig carries the effort knob used by adaptive-thinking | |
| 174 | + | // models to control reasoning depth ("low"|"medium"|"high"|"xhigh"). | |
| 175 | + | type anthOutputConfig struct { | |
| 176 | + | Effort string `json:"effort,omitempty"` | |
| 167 | 177 | } | |
| 168 | 178 | ||
| 169 | 179 | type anthRequest struct { | |
| 170 | - | Model string `json:"model"` | |
| 171 | - | MaxTokens int `json:"max_tokens"` | |
| 172 | - | System []anthSystemBlock `json:"system,omitempty"` | |
| 173 | - | Messages []anthMessage `json:"messages"` | |
| 174 | - | Tools []anthTool `json:"tools,omitempty"` | |
| 175 | - | Temperature *float32 `json:"temperature,omitempty"` | |
| 176 | - | Thinking *anthThinking `json:"thinking,omitempty"` | |
| 177 | - | Stream bool `json:"stream"` | |
| 180 | + | Model string `json:"model"` | |
| 181 | + | MaxTokens int `json:"max_tokens"` | |
| 182 | + | System []anthSystemBlock `json:"system,omitempty"` | |
| 183 | + | Messages []anthMessage `json:"messages"` | |
| 184 | + | Tools []anthTool `json:"tools,omitempty"` | |
| 185 | + | Temperature *float32 `json:"temperature,omitempty"` | |
| 186 | + | Thinking *anthThinking `json:"thinking,omitempty"` | |
| 187 | + | OutputConfig *anthOutputConfig `json:"output_config,omitempty"` | |
| 188 | + | Stream bool `json:"stream"` | |
| 189 | + | } | |
| 190 | + | ||
| 191 | + | // usesAdaptiveThinking reports whether a model only supports the | |
| 192 | + | // adaptive thinking mode (Opus 4.7 and later). These models reject | |
| 193 | + | // explicit thinking budgets and non-default sampling parameters. The | |
| 194 | + | // catalog flag is authoritative; the id-substring fallback catches | |
| 195 | + | // the same family when reached through an Anthropic-Messages- | |
| 196 | + | // compatible proxy whose catalog row predates the flag. | |
| 197 | + | func usesAdaptiveThinking(m Model) bool { | |
| 198 | + | if m.AdaptiveThinking { | |
| 199 | + | return true | |
| 200 | + | } | |
| 201 | + | id := strings.ToLower(m.ID) | |
| 202 | + | for _, marker := range []string{"opus-4-7", "opus-4.7", "opus-4-8", "opus-4.8"} { | |
| 203 | + | if strings.Contains(id, marker) { | |
| 204 | + | return true | |
| 205 | + | } | |
| 206 | + | } | |
| 207 | + | return false | |
| 178 | 208 | } | |
| 179 | 209 | ||
| 180 | 210 | // ---- request building ---- | |
| @@ -200,11 +230,18 @@ func (c *anthropicClient) buildRequest(req Request) (*anthRequest, error) { | |||
| 200 | 230 | maxTok = m.MaxOutput | |
| 201 | 231 | } | |
| 202 | 232 | ||
| 233 | + | adaptive := usesAdaptiveThinking(m) | |
| 234 | + | ||
| 203 | 235 | out := &anthRequest{ | |
| 204 | - | Model: req.Model, | |
| 205 | - | MaxTokens: maxTok, | |
| 206 | - | Temperature: req.Temperature, | |
| 207 | - | Stream: true, | |
| 236 | + | Model: req.Model, | |
| 237 | + | MaxTokens: maxTok, | |
| 238 | + | Stream: true, | |
| 239 | + | } | |
| 240 | + | // Adaptive-thinking models reject non-default sampling params | |
| 241 | + | // (temperature/top_p/top_k -> 400). Only forward temperature for | |
| 242 | + | // models that accept it. | |
| 243 | + | if !adaptive { | |
| 244 | + | out.Temperature = req.Temperature | |
| 208 | 245 | } | |
| 209 | 246 | ||
| 210 | 247 | // System prompt assembly differs between api-key and OAuth modes. | |
| @@ -247,19 +284,29 @@ func (c *anthropicClient) buildRequest(req Request) (*anthRequest, error) { | |||
| 247 | 284 | } | |
| 248 | 285 | ||
| 249 | 286 | if req.Reasoning != "" && m.Reasoning { | |
| 250 | - | budget := anthropicReasoningBudget(req.Reasoning) | |
| 251 | - | if budget > 0 { | |
| 252 | - | // Reasoning requires max_tokens > budget. Keep at least a small | |
| 253 | - | // answer budget while respecting the model's advertised output cap. | |
| 254 | - | const minAnswerTokens = 1024 | |
| 255 | - | if m.MaxOutput > minAnswerTokens && budget >= m.MaxOutput { | |
| 256 | - | budget = m.MaxOutput - minAnswerTokens | |
| 287 | + | if adaptive { | |
| 288 | + | // Adaptive-thinking models (Opus 4.7+): the model decides when | |
| 289 | + | // and how much to think; depth is steered by output_config.effort. | |
| 290 | + | // Explicit budgets are rejected with a 400, so none is sent. | |
| 291 | + | out.Thinking = &anthThinking{Type: "adaptive"} | |
| 292 | + | if effort := AnthropicAdaptiveEffort(req.Reasoning); effort != "" { | |
| 293 | + | out.OutputConfig = &anthOutputConfig{Effort: effort} | |
| 257 | 294 | } | |
| 258 | - | out.Thinking = &anthThinking{Type: "enabled", BudgetTokens: budget} | |
| 259 | - | if out.MaxTokens <= budget { | |
| 260 | - | out.MaxTokens = budget + minAnswerTokens | |
| 261 | - | if m.MaxOutput > 0 && out.MaxTokens > m.MaxOutput { | |
| 262 | - | out.MaxTokens = m.MaxOutput | |
| 295 | + | } else { | |
| 296 | + | budget := anthropicReasoningBudget(req.Reasoning) | |
| 297 | + | if budget > 0 { | |
| 298 | + | // Reasoning requires max_tokens > budget. Keep at least a small | |
| 299 | + | // answer budget while respecting the model's advertised output cap. | |
| 300 | + | const minAnswerTokens = 1024 | |
| 301 | + | if m.MaxOutput > minAnswerTokens && budget >= m.MaxOutput { | |
| 302 | + | budget = m.MaxOutput - minAnswerTokens | |
| 303 | + | } | |
| 304 | + | out.Thinking = &anthThinking{Type: "enabled", BudgetTokens: budget} | |
| 305 | + | if out.MaxTokens <= budget { | |
| 306 | + | out.MaxTokens = budget + minAnswerTokens | |
| 307 | + | if m.MaxOutput > 0 && out.MaxTokens > m.MaxOutput { | |
| 308 | + | out.MaxTokens = m.MaxOutput | |
| 309 | + | } | |
| 263 | 310 | } | |
| 264 | 311 | } | |
| 265 | 312 | } | |
| @@ -162,19 +162,49 @@ type anthTool struct { | |||||
| 162 | } | 162 | } | ||
| 163 | 163 | ||||
| 164 | type anthThinking struct { | 164 | type anthThinking struct { | ||
| 165 | - | Type string `json:"type"` // "enabled" | 165 | + | // Type is "enabled" for budget-based thinking (Opus 4.6 and |
| 166 | - | BudgetTokens int `json:"budget_tokens"` | 166 | + | // earlier) or "adaptive" for adaptive-thinking models (Opus 4.7+). |
| 167 | + | Type string `json:"type"` | |||
| 168 | + | // BudgetTokens is only sent for Type=="enabled". Adaptive models | |||
| 169 | + | // reject it (400), so it is omitted for them. | |||
| 170 | + | BudgetTokens int `json:"budget_tokens,omitempty"` | |||
| 171 | + | } | |||
| 172 | + | ||||
| 173 | + | // anthOutputConfig carries the effort knob used by adaptive-thinking | |||
| 174 | + | // models to control reasoning depth ("low"|"medium"|"high"|"xhigh"). | |||
| 175 | + | type anthOutputConfig struct { | |||
| 176 | + | Effort string `json:"effort,omitempty"` | |||
| 167 | } | 177 | } | ||
| 168 | 178 | ||||
| 169 | type anthRequest struct { | 179 | type anthRequest struct { | ||
| 170 | - | Model string `json:"model"` | 180 | + | Model string `json:"model"` |
| 171 | - | MaxTokens int `json:"max_tokens"` | 181 | + | MaxTokens int `json:"max_tokens"` |
| 172 | - | System []anthSystemBlock `json:"system,omitempty"` | 182 | + | System []anthSystemBlock `json:"system,omitempty"` |
| 173 | - | Messages []anthMessage `json:"messages"` | 183 | + | Messages []anthMessage `json:"messages"` |
| 174 | - | Tools []anthTool `json:"tools,omitempty"` | 184 | + | Tools []anthTool `json:"tools,omitempty"` |
| 175 | - | Temperature *float32 `json:"temperature,omitempty"` | 185 | + | Temperature *float32 `json:"temperature,omitempty"` |
| 176 | - | Thinking *anthThinking `json:"thinking,omitempty"` | 186 | + | Thinking *anthThinking `json:"thinking,omitempty"` |
| 177 | - | Stream bool `json:"stream"` | 187 | + | OutputConfig *anthOutputConfig `json:"output_config,omitempty"` |
| 188 | + | Stream bool `json:"stream"` | |||
| 189 | + | } | |||
| 190 | + | ||||
| 191 | + | // usesAdaptiveThinking reports whether a model only supports the | |||
| 192 | + | // adaptive thinking mode (Opus 4.7 and later). These models reject | |||
| 193 | + | // explicit thinking budgets and non-default sampling parameters. The | |||
| 194 | + | // catalog flag is authoritative; the id-substring fallback catches | |||
| 195 | + | // the same family when reached through an Anthropic-Messages- | |||
| 196 | + | // compatible proxy whose catalog row predates the flag. | |||
| 197 | + | func usesAdaptiveThinking(m Model) bool { | |||
| 198 | + | if m.AdaptiveThinking { | |||
| 199 | + | return true | |||
| 200 | + | } | |||
| 201 | + | id := strings.ToLower(m.ID) | |||
| 202 | + | for _, marker := range []string{"opus-4-7", "opus-4.7", "opus-4-8", "opus-4.8"} { | |||
| 203 | + | if strings.Contains(id, marker) { | |||
| 204 | + | return true | |||
| 205 | + | } | |||
| 206 | + | } | |||
| 207 | + | return false | |||
| 178 | } | 208 | } | ||
| 179 | 209 | ||||
| 180 | // ---- request building ---- | 210 | // ---- request building ---- | ||
| @@ -200,11 +230,18 @@ func (c *anthropicClient) buildRequest(req Request) (*anthRequest, error) { | |||||
| 200 | maxTok = m.MaxOutput | 230 | maxTok = m.MaxOutput | ||
| 201 | } | 231 | } | ||
| 202 | 232 | ||||
| 233 | + | adaptive := usesAdaptiveThinking(m) | |||
| 234 | + | ||||
| 203 | out := &anthRequest{ | 235 | out := &anthRequest{ | ||
| 204 | - | Model: req.Model, | 236 | + | Model: req.Model, |
| 205 | - | MaxTokens: maxTok, | 237 | + | MaxTokens: maxTok, |
| 206 | - | Temperature: req.Temperature, | 238 | + | Stream: true, |
| 207 | - | Stream: true, | 239 | + | } |
| 240 | + | // Adaptive-thinking models reject non-default sampling params | |||
| 241 | + | // (temperature/top_p/top_k -> 400). Only forward temperature for | |||
| 242 | + | // models that accept it. | |||
| 243 | + | if !adaptive { | |||
| 244 | + | out.Temperature = req.Temperature | |||
| 208 | } | 245 | } | ||
| 209 | 246 | ||||
| 210 | // System prompt assembly differs between api-key and OAuth modes. | 247 | // System prompt assembly differs between api-key and OAuth modes. | ||
| @@ -247,19 +284,29 @@ func (c *anthropicClient) buildRequest(req Request) (*anthRequest, error) { | |||||
| 247 | } | 284 | } | ||
| 248 | 285 | ||||
| 249 | if req.Reasoning != "" && m.Reasoning { | 286 | if req.Reasoning != "" && m.Reasoning { | ||
| 250 | - | budget := anthropicReasoningBudget(req.Reasoning) | 287 | + | if adaptive { |
| 251 | - | if budget > 0 { | 288 | + | // Adaptive-thinking models (Opus 4.7+): the model decides when |
| 252 | - | // Reasoning requires max_tokens > budget. Keep at least a small | 289 | + | // and how much to think; depth is steered by output_config.effort. |
| 253 | - | // answer budget while respecting the model's advertised output cap. | 290 | + | // Explicit budgets are rejected with a 400, so none is sent. |
| 254 | - | const minAnswerTokens = 1024 | 291 | + | out.Thinking = &anthThinking{Type: "adaptive"} |
| 255 | - | if m.MaxOutput > minAnswerTokens && budget >= m.MaxOutput { | 292 | + | if effort := AnthropicAdaptiveEffort(req.Reasoning); effort != "" { |
| 256 | - | budget = m.MaxOutput - minAnswerTokens | 293 | + | out.OutputConfig = &anthOutputConfig{Effort: effort} |
| 257 | } | 294 | } | ||
| 258 | - | out.Thinking = &anthThinking{Type: "enabled", BudgetTokens: budget} | 295 | + | } else { |
| 259 | - | if out.MaxTokens <= budget { | 296 | + | budget := anthropicReasoningBudget(req.Reasoning) |
| 260 | - | out.MaxTokens = budget + minAnswerTokens | 297 | + | if budget > 0 { |
| 261 | - | if m.MaxOutput > 0 && out.MaxTokens > m.MaxOutput { | 298 | + | // Reasoning requires max_tokens > budget. Keep at least a small |
| 262 | - | out.MaxTokens = m.MaxOutput | 299 | + | // answer budget while respecting the model's advertised output cap. |
| 300 | + | const minAnswerTokens = 1024 | |||
| 301 | + | if m.MaxOutput > minAnswerTokens && budget >= m.MaxOutput { | |||
| 302 | + | budget = m.MaxOutput - minAnswerTokens | |||
| 303 | + | } | |||
| 304 | + | out.Thinking = &anthThinking{Type: "enabled", BudgetTokens: budget} | |||
| 305 | + | if out.MaxTokens <= budget { | |||
| 306 | + | out.MaxTokens = budget + minAnswerTokens | |||
| 307 | + | if m.MaxOutput > 0 && out.MaxTokens > m.MaxOutput { | |||
| 308 | + | out.MaxTokens = m.MaxOutput | |||
| 309 | + | } | |||
| 263 | } | 310 | } | ||
| 264 | } | 311 | } | ||
| 265 | } | 312 | } | ||
M packages/provider/models.go +10 -2
| @@ -14,6 +14,14 @@ type Model struct { | |||
| 14 | 14 | MaxOutput int | |
| 15 | 15 | Reasoning bool // supports reasoning/thinking | |
| 16 | 16 | ||
| 17 | + | // AdaptiveThinking marks Anthropic models that only support the | |
| 18 | + | // adaptive thinking mode (Opus 4.7+). These reject explicit | |
| 19 | + | // thinking budgets (thinking:{type:"enabled",budget_tokens:N} -> 400) | |
| 20 | + | // and also reject non-default sampling params (temperature/top_p/ | |
| 21 | + | // top_k). The Anthropic client sends thinking:{type:"adaptive"} plus | |
| 22 | + | // output_config.effort and omits temperature for these models. | |
| 23 | + | AdaptiveThinking bool | |
| 24 | + | ||
| 17 | 25 | // Prices are USD per 1M tokens. | |
| 18 | 26 | PriceInput float64 | |
| 19 | 27 | PriceOutput float64 | |
| @@ -230,13 +238,13 @@ var Catalog = []Model{ | |||
| 230 | 238 | }, | |
| 231 | 239 | { | |
| 232 | 240 | Provider: "anthropic", ID: "claude-opus-4-7", DisplayName: "Claude Opus 4.7", | |
| 233 | - | ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, | |
| 241 | + | ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, | |
| 234 | 242 | PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, | |
| 235 | 243 | Speculative: true, | |
| 236 | 244 | }, | |
| 237 | 245 | { | |
| 238 | 246 | Provider: "anthropic", ID: "claude-opus-4-8", DisplayName: "Claude Opus 4.8", | |
| 239 | - | ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, | |
| 247 | + | ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, | |
| 240 | 248 | PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, | |
| 241 | 249 | Speculative: true, | |
| 242 | 250 | }, | |
| @@ -14,6 +14,14 @@ type Model struct { | |||||
| 14 | MaxOutput int | 14 | MaxOutput int | ||
| 15 | Reasoning bool // supports reasoning/thinking | 15 | Reasoning bool // supports reasoning/thinking | ||
| 16 | 16 | ||||
| 17 | + | // AdaptiveThinking marks Anthropic models that only support the | |||
| 18 | + | // adaptive thinking mode (Opus 4.7+). These reject explicit | |||
| 19 | + | // thinking budgets (thinking:{type:"enabled",budget_tokens:N} -> 400) | |||
| 20 | + | // and also reject non-default sampling params (temperature/top_p/ | |||
| 21 | + | // top_k). The Anthropic client sends thinking:{type:"adaptive"} plus | |||
| 22 | + | // output_config.effort and omits temperature for these models. | |||
| 23 | + | AdaptiveThinking bool | |||
| 24 | + | ||||
| 17 | // Prices are USD per 1M tokens. | 25 | // Prices are USD per 1M tokens. | ||
| 18 | PriceInput float64 | 26 | PriceInput float64 | ||
| 19 | PriceOutput float64 | 27 | PriceOutput float64 | ||
| @@ -230,13 +238,13 @@ var Catalog = []Model{ | |||||
| 230 | }, | 238 | }, | ||
| 231 | { | 239 | { | ||
| 232 | Provider: "anthropic", ID: "claude-opus-4-7", DisplayName: "Claude Opus 4.7", | 240 | Provider: "anthropic", ID: "claude-opus-4-7", DisplayName: "Claude Opus 4.7", | ||
| 233 | - | ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, | 241 | + | ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, |
| 234 | PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, | 242 | PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, | ||
| 235 | Speculative: true, | 243 | Speculative: true, | ||
| 236 | }, | 244 | }, | ||
| 237 | { | 245 | { | ||
| 238 | Provider: "anthropic", ID: "claude-opus-4-8", DisplayName: "Claude Opus 4.8", | 246 | Provider: "anthropic", ID: "claude-opus-4-8", DisplayName: "Claude Opus 4.8", | ||
| 239 | - | ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, | 247 | + | ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, |
| 240 | PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, | 248 | PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, | ||
| 241 | Speculative: true, | 249 | Speculative: true, | ||
| 242 | }, | 250 | }, | ||
M packages/provider/openai.go +9 -1
| @@ -185,7 +185,15 @@ func (c *openaiClient) buildRequest(req Request) (*oaiRequest, error) { | |||
| 185 | 185 | } | |
| 186 | 186 | if m.Reasoning { | |
| 187 | 187 | out.MaxCompletionTok = &maxTok | |
| 188 | - | if effort := OpenAIReasoningEffort(req.Reasoning); effort != "" { | |
| 188 | + | effort := OpenAIReasoningEffort(req.Reasoning) | |
| 189 | + | if usesAdaptiveThinking(m) { | |
| 190 | + | // Some gateways expose adaptive-thinking Anthropic models through | |
| 191 | + | // the OpenAI-compatible chat-completions wire. They accept the | |
| 192 | + | // same reasoning_effort knob, including the top "xhigh" tier; | |
| 193 | + | // don't clamp zot's "maximum" to "high" for those models. | |
| 194 | + | effort = OpenAICompatAnthropicEffort(req.Reasoning) | |
| 195 | + | } | |
| 196 | + | if effort != "" { | |
| 189 | 197 | out.ReasoningEffort = effort | |
| 190 | 198 | } | |
| 191 | 199 | } else { | |
| @@ -185,7 +185,15 @@ func (c *openaiClient) buildRequest(req Request) (*oaiRequest, error) { | |||||
| 185 | } | 185 | } | ||
| 186 | if m.Reasoning { | 186 | if m.Reasoning { | ||
| 187 | out.MaxCompletionTok = &maxTok | 187 | out.MaxCompletionTok = &maxTok | ||
| 188 | - | if effort := OpenAIReasoningEffort(req.Reasoning); effort != "" { | 188 | + | effort := OpenAIReasoningEffort(req.Reasoning) |
| 189 | + | if usesAdaptiveThinking(m) { | |||
| 190 | + | // Some gateways expose adaptive-thinking Anthropic models through | |||
| 191 | + | // the OpenAI-compatible chat-completions wire. They accept the | |||
| 192 | + | // same reasoning_effort knob, including the top "xhigh" tier; | |||
| 193 | + | // don't clamp zot's "maximum" to "high" for those models. | |||
| 194 | + | effort = OpenAICompatAnthropicEffort(req.Reasoning) | |||
| 195 | + | } | |||
| 196 | + | if effort != "" { | |||
| 189 | out.ReasoningEffort = effort | 197 | out.ReasoningEffort = effort | ||
| 190 | } | 198 | } | ||
| 191 | } else { | 199 | } else { | ||
M packages/provider/provider_test.go +88 -0
| @@ -78,6 +78,67 @@ func TestOpenAIErrorStatus(t *testing.T) { | |||
| 78 | 78 | } | |
| 79 | 79 | } | |
| 80 | 80 | ||
| 81 | + | func TestAnthropicAdaptiveThinking(t *testing.T) { | |
| 82 | + | c := NewAnthropic("x", "").(*anthropicClient) | |
| 83 | + | temp := float32(0.7) | |
| 84 | + | ||
| 85 | + | // Opus 4.8 -> adaptive thinking, effort set, no budget, no temperature. | |
| 86 | + | wire, err := c.buildRequest(Request{ | |
| 87 | + | Model: "claude-opus-4-8", | |
| 88 | + | Reasoning: "high", | |
| 89 | + | Temperature: &temp, | |
| 90 | + | Messages: []Message{{Role: RoleUser, Content: []Content{TextBlock{Text: "hi"}}}}, | |
| 91 | + | }) | |
| 92 | + | if err != nil { | |
| 93 | + | t.Fatal(err) | |
| 94 | + | } | |
| 95 | + | if wire.Thinking == nil || wire.Thinking.Type != "adaptive" { | |
| 96 | + | t.Fatalf("want adaptive thinking, got %+v", wire.Thinking) | |
| 97 | + | } | |
| 98 | + | if wire.Thinking.BudgetTokens != 0 { | |
| 99 | + | t.Fatalf("adaptive must not send budget_tokens, got %d", wire.Thinking.BudgetTokens) | |
| 100 | + | } | |
| 101 | + | if wire.OutputConfig == nil || wire.OutputConfig.Effort != "high" { | |
| 102 | + | t.Fatalf("want effort=high, got %+v", wire.OutputConfig) | |
| 103 | + | } | |
| 104 | + | if wire.Temperature != nil { | |
| 105 | + | t.Fatalf("adaptive must drop temperature, got %v", *wire.Temperature) | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | // maximum -> xhigh effort. | |
| 109 | + | wire, err = c.buildRequest(Request{ | |
| 110 | + | Model: "claude-opus-4-8", | |
| 111 | + | Reasoning: "maximum", | |
| 112 | + | Messages: []Message{{Role: RoleUser, Content: []Content{TextBlock{Text: "hi"}}}}, | |
| 113 | + | }) | |
| 114 | + | if err != nil { | |
| 115 | + | t.Fatal(err) | |
| 116 | + | } | |
| 117 | + | if wire.OutputConfig == nil || wire.OutputConfig.Effort != "xhigh" { | |
| 118 | + | t.Fatalf("want effort=xhigh, got %+v", wire.OutputConfig) | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | // Opus 4.5 -> budget-based thinking, no output_config, temperature kept. | |
| 122 | + | wire, err = c.buildRequest(Request{ | |
| 123 | + | Model: "claude-opus-4-5", | |
| 124 | + | Reasoning: "high", | |
| 125 | + | Temperature: &temp, | |
| 126 | + | Messages: []Message{{Role: RoleUser, Content: []Content{TextBlock{Text: "hi"}}}}, | |
| 127 | + | }) | |
| 128 | + | if err != nil { | |
| 129 | + | t.Fatal(err) | |
| 130 | + | } | |
| 131 | + | if wire.Thinking == nil || wire.Thinking.Type != "enabled" || wire.Thinking.BudgetTokens <= 0 { | |
| 132 | + | t.Fatalf("want budget thinking, got %+v", wire.Thinking) | |
| 133 | + | } | |
| 134 | + | if wire.OutputConfig != nil { | |
| 135 | + | t.Fatalf("budget models must not send output_config, got %+v", wire.OutputConfig) | |
| 136 | + | } | |
| 137 | + | if wire.Temperature == nil || *wire.Temperature != temp { | |
| 138 | + | t.Fatalf("budget model should keep temperature, got %v", wire.Temperature) | |
| 139 | + | } | |
| 140 | + | } | |
| 141 | + | ||
| 81 | 142 | func TestAnthropicBuildRequestStripsAssistantImages(t *testing.T) { | |
| 82 | 143 | c := NewAnthropic("x", "").(*anthropicClient) | |
| 83 | 144 | wire, err := c.buildRequest(Request{ | |
| @@ -155,6 +216,33 @@ func TestAnthropicStreamHappyPath(t *testing.T) { | |||
| 155 | 216 | } | |
| 156 | 217 | } | |
| 157 | 218 | ||
| 219 | + | func TestOpenAICompatAnthropicReasoningEffort(t *testing.T) { | |
| 220 | + | c := NewOpenRouter("token", "").(*openaiClient) | |
| 221 | + | wire, err := c.buildRequest(Request{ | |
| 222 | + | Model: "anthropic/claude-opus-4.8", | |
| 223 | + | Reasoning: "maximum", | |
| 224 | + | Messages: []Message{{Role: RoleUser, Content: []Content{TextBlock{Text: "hi"}}}}, | |
| 225 | + | }) | |
| 226 | + | if err != nil { | |
| 227 | + | t.Fatal(err) | |
| 228 | + | } | |
| 229 | + | if wire.ReasoningEffort != "xhigh" { | |
| 230 | + | t.Fatalf("want xhigh for adaptive Anthropic model over OpenAI-compatible wire, got %q", wire.ReasoningEffort) | |
| 231 | + | } | |
| 232 | + | ||
| 233 | + | wire, err = c.buildRequest(Request{ | |
| 234 | + | Model: "gpt-5.1", | |
| 235 | + | Reasoning: "maximum", | |
| 236 | + | Messages: []Message{{Role: RoleUser, Content: []Content{TextBlock{Text: "hi"}}}}, | |
| 237 | + | }) | |
| 238 | + | if err != nil { | |
| 239 | + | t.Fatal(err) | |
| 240 | + | } | |
| 241 | + | if wire.ReasoningEffort != "high" { | |
| 242 | + | t.Fatalf("want generic OpenAI-compatible maximum clamped to high, got %q", wire.ReasoningEffort) | |
| 243 | + | } | |
| 244 | + | } | |
| 245 | + | ||
| 158 | 246 | func TestOpenAIBuildRequestSkipsReasoningOnlyAssistantMessages(t *testing.T) { | |
| 159 | 247 | c := NewKimi("token", "").(*openaiClient) | |
| 160 | 248 | wire, err := c.buildRequest(Request{ | |
| @@ -78,6 +78,67 @@ func TestOpenAIErrorStatus(t *testing.T) { | |||||
| 78 | } | 78 | } | ||
| 79 | } | 79 | } | ||
| 80 | 80 | ||||
| 81 | + | func TestAnthropicAdaptiveThinking(t *testing.T) { | |||
| 82 | + | c := NewAnthropic("x", "").(*anthropicClient) | |||
| 83 | + | temp := float32(0.7) | |||
| 84 | + | ||||
| 85 | + | // Opus 4.8 -> adaptive thinking, effort set, no budget, no temperature. | |||
| 86 | + | wire, err := c.buildRequest(Request{ | |||
| 87 | + | Model: "claude-opus-4-8", | |||
| 88 | + | Reasoning: "high", | |||
| 89 | + | Temperature: &temp, | |||
| 90 | + | Messages: []Message{{Role: RoleUser, Content: []Content{TextBlock{Text: "hi"}}}}, | |||
| 91 | + | }) | |||
| 92 | + | if err != nil { | |||
| 93 | + | t.Fatal(err) | |||
| 94 | + | } | |||
| 95 | + | if wire.Thinking == nil || wire.Thinking.Type != "adaptive" { | |||
| 96 | + | t.Fatalf("want adaptive thinking, got %+v", wire.Thinking) | |||
| 97 | + | } | |||
| 98 | + | if wire.Thinking.BudgetTokens != 0 { | |||
| 99 | + | t.Fatalf("adaptive must not send budget_tokens, got %d", wire.Thinking.BudgetTokens) | |||
| 100 | + | } | |||
| 101 | + | if wire.OutputConfig == nil || wire.OutputConfig.Effort != "high" { | |||
| 102 | + | t.Fatalf("want effort=high, got %+v", wire.OutputConfig) | |||
| 103 | + | } | |||
| 104 | + | if wire.Temperature != nil { | |||
| 105 | + | t.Fatalf("adaptive must drop temperature, got %v", *wire.Temperature) | |||
| 106 | + | } | |||
| 107 | + | ||||
| 108 | + | // maximum -> xhigh effort. | |||
| 109 | + | wire, err = c.buildRequest(Request{ | |||
| 110 | + | Model: "claude-opus-4-8", | |||
| 111 | + | Reasoning: "maximum", | |||
| 112 | + | Messages: []Message{{Role: RoleUser, Content: []Content{TextBlock{Text: "hi"}}}}, | |||
| 113 | + | }) | |||
| 114 | + | if err != nil { | |||
| 115 | + | t.Fatal(err) | |||
| 116 | + | } | |||
| 117 | + | if wire.OutputConfig == nil || wire.OutputConfig.Effort != "xhigh" { | |||
| 118 | + | t.Fatalf("want effort=xhigh, got %+v", wire.OutputConfig) | |||
| 119 | + | } | |||
| 120 | + | ||||
| 121 | + | // Opus 4.5 -> budget-based thinking, no output_config, temperature kept. | |||
| 122 | + | wire, err = c.buildRequest(Request{ | |||
| 123 | + | Model: "claude-opus-4-5", | |||
| 124 | + | Reasoning: "high", | |||
| 125 | + | Temperature: &temp, | |||
| 126 | + | Messages: []Message{{Role: RoleUser, Content: []Content{TextBlock{Text: "hi"}}}}, | |||
| 127 | + | }) | |||
| 128 | + | if err != nil { | |||
| 129 | + | t.Fatal(err) | |||
| 130 | + | } | |||
| 131 | + | if wire.Thinking == nil || wire.Thinking.Type != "enabled" || wire.Thinking.BudgetTokens <= 0 { | |||
| 132 | + | t.Fatalf("want budget thinking, got %+v", wire.Thinking) | |||
| 133 | + | } | |||
| 134 | + | if wire.OutputConfig != nil { | |||
| 135 | + | t.Fatalf("budget models must not send output_config, got %+v", wire.OutputConfig) | |||
| 136 | + | } | |||
| 137 | + | if wire.Temperature == nil || *wire.Temperature != temp { | |||
| 138 | + | t.Fatalf("budget model should keep temperature, got %v", wire.Temperature) | |||
| 139 | + | } | |||
| 140 | + | } | |||
| 141 | + | ||||
| 81 | func TestAnthropicBuildRequestStripsAssistantImages(t *testing.T) { | 142 | func TestAnthropicBuildRequestStripsAssistantImages(t *testing.T) { | ||
| 82 | c := NewAnthropic("x", "").(*anthropicClient) | 143 | c := NewAnthropic("x", "").(*anthropicClient) | ||
| 83 | wire, err := c.buildRequest(Request{ | 144 | wire, err := c.buildRequest(Request{ | ||
| @@ -155,6 +216,33 @@ func TestAnthropicStreamHappyPath(t *testing.T) { | |||||
| 155 | } | 216 | } | ||
| 156 | } | 217 | } | ||
| 157 | 218 | ||||
| 219 | + | func TestOpenAICompatAnthropicReasoningEffort(t *testing.T) { | |||
| 220 | + | c := NewOpenRouter("token", "").(*openaiClient) | |||
| 221 | + | wire, err := c.buildRequest(Request{ | |||
| 222 | + | Model: "anthropic/claude-opus-4.8", | |||
| 223 | + | Reasoning: "maximum", | |||
| 224 | + | Messages: []Message{{Role: RoleUser, Content: []Content{TextBlock{Text: "hi"}}}}, | |||
| 225 | + | }) | |||
| 226 | + | if err != nil { | |||
| 227 | + | t.Fatal(err) | |||
| 228 | + | } | |||
| 229 | + | if wire.ReasoningEffort != "xhigh" { | |||
| 230 | + | t.Fatalf("want xhigh for adaptive Anthropic model over OpenAI-compatible wire, got %q", wire.ReasoningEffort) | |||
| 231 | + | } | |||
| 232 | + | ||||
| 233 | + | wire, err = c.buildRequest(Request{ | |||
| 234 | + | Model: "gpt-5.1", | |||
| 235 | + | Reasoning: "maximum", | |||
| 236 | + | Messages: []Message{{Role: RoleUser, Content: []Content{TextBlock{Text: "hi"}}}}, | |||
| 237 | + | }) | |||
| 238 | + | if err != nil { | |||
| 239 | + | t.Fatal(err) | |||
| 240 | + | } | |||
| 241 | + | if wire.ReasoningEffort != "high" { | |||
| 242 | + | t.Fatalf("want generic OpenAI-compatible maximum clamped to high, got %q", wire.ReasoningEffort) | |||
| 243 | + | } | |||
| 244 | + | } | |||
| 245 | + | ||||
| 158 | func TestOpenAIBuildRequestSkipsReasoningOnlyAssistantMessages(t *testing.T) { | 246 | func TestOpenAIBuildRequestSkipsReasoningOnlyAssistantMessages(t *testing.T) { | ||
| 159 | c := NewKimi("token", "").(*openaiClient) | 247 | c := NewKimi("token", "").(*openaiClient) | ||
| 160 | wire, err := c.buildRequest(Request{ | 248 | wire, err := c.buildRequest(Request{ | ||
M packages/provider/reasoning.go +43 -0
| @@ -42,6 +42,26 @@ func ReasoningBudget(level string) int { | |||
| 42 | 42 | } | |
| 43 | 43 | } | |
| 44 | 44 | ||
| 45 | + | // AnthropicAdaptiveEffort maps zot's user-facing thinking levels onto the | |
| 46 | + | // effort enum used by Anthropic's adaptive-thinking models (Opus 4.7+). | |
| 47 | + | // These models reject explicit thinking budgets; thinking depth is | |
| 48 | + | // controlled by output_config.effort instead. Returns "" when reasoning | |
| 49 | + | // is disabled. | |
| 50 | + | func AnthropicAdaptiveEffort(level string) string { | |
| 51 | + | switch NormalizeReasoning(level) { | |
| 52 | + | case "minimum", "low": | |
| 53 | + | return "low" | |
| 54 | + | case "medium": | |
| 55 | + | return "medium" | |
| 56 | + | case "high": | |
| 57 | + | return "high" | |
| 58 | + | case "maximum": | |
| 59 | + | return "xhigh" | |
| 60 | + | default: | |
| 61 | + | return "" | |
| 62 | + | } | |
| 63 | + | } | |
| 64 | + | ||
| 45 | 65 | // OpenAIReasoningEffort maps zot's six-level setting onto the effort enum | |
| 46 | 66 | // accepted by OpenAI-compatible chat-completions endpoints. | |
| 47 | 67 | func OpenAIReasoningEffort(level string) string { | |
| @@ -59,6 +79,29 @@ func OpenAIReasoningEffort(level string) string { | |||
| 59 | 79 | } | |
| 60 | 80 | } | |
| 61 | 81 | ||
| 82 | + | // OpenAICompatAnthropicEffort maps zot's user-facing thinking levels | |
| 83 | + | // onto reasoning_effort values when an adaptive-thinking Anthropic | |
| 84 | + | // model (Opus 4.7+) is served over the OpenAI-compatible chat- | |
| 85 | + | // completions wire (openrouter, opencode, ...). Differs from | |
| 86 | + | // OpenAIReasoningEffort only at the top: zot's "maximum" maps to | |
| 87 | + | // "xhigh" instead of being clamped to "high", so the model's full | |
| 88 | + | // adaptive-thinking ceiling is preserved when reachable through a | |
| 89 | + | // gateway that accepts the effort knob. | |
| 90 | + | func OpenAICompatAnthropicEffort(level string) string { | |
| 91 | + | switch NormalizeReasoning(level) { | |
| 92 | + | case "minimum", "low": | |
| 93 | + | return "low" | |
| 94 | + | case "medium": | |
| 95 | + | return "medium" | |
| 96 | + | case "high": | |
| 97 | + | return "high" | |
| 98 | + | case "maximum": | |
| 99 | + | return "xhigh" | |
| 100 | + | default: | |
| 101 | + | return "" | |
| 102 | + | } | |
| 103 | + | } | |
| 104 | + | ||
| 62 | 105 | // OpenAICodexReasoningEffort maps zot levels onto the ChatGPT/Codex | |
| 63 | 106 | // Responses backend enum. That backend rejects "minimal" and uses | |
| 64 | 107 | // "xhigh" for the highest tier on recent GPT-5.x models. | |
| @@ -42,6 +42,26 @@ func ReasoningBudget(level string) int { | |||||
| 42 | } | 42 | } | ||
| 43 | } | 43 | } | ||
| 44 | 44 | ||||
| 45 | + | // AnthropicAdaptiveEffort maps zot's user-facing thinking levels onto the | |||
| 46 | + | // effort enum used by Anthropic's adaptive-thinking models (Opus 4.7+). | |||
| 47 | + | // These models reject explicit thinking budgets; thinking depth is | |||
| 48 | + | // controlled by output_config.effort instead. Returns "" when reasoning | |||
| 49 | + | // is disabled. | |||
| 50 | + | func AnthropicAdaptiveEffort(level string) string { | |||
| 51 | + | switch NormalizeReasoning(level) { | |||
| 52 | + | case "minimum", "low": | |||
| 53 | + | return "low" | |||
| 54 | + | case "medium": | |||
| 55 | + | return "medium" | |||
| 56 | + | case "high": | |||
| 57 | + | return "high" | |||
| 58 | + | case "maximum": | |||
| 59 | + | return "xhigh" | |||
| 60 | + | default: | |||
| 61 | + | return "" | |||
| 62 | + | } | |||
| 63 | + | } | |||
| 64 | + | ||||
| 45 | // OpenAIReasoningEffort maps zot's six-level setting onto the effort enum | 65 | // OpenAIReasoningEffort maps zot's six-level setting onto the effort enum | ||
| 46 | // accepted by OpenAI-compatible chat-completions endpoints. | 66 | // accepted by OpenAI-compatible chat-completions endpoints. | ||
| 47 | func OpenAIReasoningEffort(level string) string { | 67 | func OpenAIReasoningEffort(level string) string { | ||
| @@ -59,6 +79,29 @@ func OpenAIReasoningEffort(level string) string { | |||||
| 59 | } | 79 | } | ||
| 60 | } | 80 | } | ||
| 61 | 81 | ||||
| 82 | + | // OpenAICompatAnthropicEffort maps zot's user-facing thinking levels | |||
| 83 | + | // onto reasoning_effort values when an adaptive-thinking Anthropic | |||
| 84 | + | // model (Opus 4.7+) is served over the OpenAI-compatible chat- | |||
| 85 | + | // completions wire (openrouter, opencode, ...). Differs from | |||
| 86 | + | // OpenAIReasoningEffort only at the top: zot's "maximum" maps to | |||
| 87 | + | // "xhigh" instead of being clamped to "high", so the model's full | |||
| 88 | + | // adaptive-thinking ceiling is preserved when reachable through a | |||
| 89 | + | // gateway that accepts the effort knob. | |||
| 90 | + | func OpenAICompatAnthropicEffort(level string) string { | |||
| 91 | + | switch NormalizeReasoning(level) { | |||
| 92 | + | case "minimum", "low": | |||
| 93 | + | return "low" | |||
| 94 | + | case "medium": | |||
| 95 | + | return "medium" | |||
| 96 | + | case "high": | |||
| 97 | + | return "high" | |||
| 98 | + | case "maximum": | |||
| 99 | + | return "xhigh" | |||
| 100 | + | default: | |||
| 101 | + | return "" | |||
| 102 | + | } | |||
| 103 | + | } | |||
| 104 | + | ||||
| 62 | // OpenAICodexReasoningEffort maps zot levels onto the ChatGPT/Codex | 105 | // OpenAICodexReasoningEffort maps zot levels onto the ChatGPT/Codex | ||
| 63 | // Responses backend enum. That backend rejects "minimal" and uses | 106 | // Responses backend enum. That backend rejects "minimal" and uses | ||
| 64 | // "xhigh" for the highest tier on recent GPT-5.x models. | 107 | // "xhigh" for the highest tier on recent GPT-5.x models. | ||
M packages/provider/reasoning_test.go +12 -8
| @@ -6,18 +6,19 @@ func TestReasoningEffortMappings(t *testing.T) { | |||
| 6 | 6 | cases := []struct { | |
| 7 | 7 | level string | |
| 8 | 8 | openai string | |
| 9 | + | anthCompat string | |
| 9 | 10 | codex string | |
| 10 | 11 | budget int | |
| 11 | 12 | normalized string | |
| 12 | 13 | }{ | |
| 13 | - | {"off", "", "", 0, ""}, | |
| 14 | - | {"minimum", "low", "low", 1024, "minimum"}, | |
| 15 | - | {"minimal", "low", "low", 1024, "minimum"}, | |
| 16 | - | {"low", "low", "low", 2048, "low"}, | |
| 17 | - | {"medium", "medium", "medium", 8192, "medium"}, | |
| 18 | - | {"high", "high", "high", 16384, "high"}, | |
| 19 | - | {"maximum", "high", "xhigh", 32768, "maximum"}, | |
| 20 | - | {"max", "high", "xhigh", 32768, "maximum"}, | |
| 14 | + | {"off", "", "", "", 0, ""}, | |
| 15 | + | {"minimum", "low", "low", "low", 1024, "minimum"}, | |
| 16 | + | {"minimal", "low", "low", "low", 1024, "minimum"}, | |
| 17 | + | {"low", "low", "low", "low", 2048, "low"}, | |
| 18 | + | {"medium", "medium", "medium", "medium", 8192, "medium"}, | |
| 19 | + | {"high", "high", "high", "high", 16384, "high"}, | |
| 20 | + | {"maximum", "high", "xhigh", "xhigh", 32768, "maximum"}, | |
| 21 | + | {"max", "high", "xhigh", "xhigh", 32768, "maximum"}, | |
| 21 | 22 | } | |
| 22 | 23 | for _, tc := range cases { | |
| 23 | 24 | if got := NormalizeReasoning(tc.level); got != tc.normalized { | |
| @@ -26,6 +27,9 @@ func TestReasoningEffortMappings(t *testing.T) { | |||
| 26 | 27 | if got := OpenAIReasoningEffort(tc.level); got != tc.openai { | |
| 27 | 28 | t.Errorf("OpenAIReasoningEffort(%q)=%q want %q", tc.level, got, tc.openai) | |
| 28 | 29 | } | |
| 30 | + | if got := OpenAICompatAnthropicEffort(tc.level); got != tc.anthCompat { | |
| 31 | + | t.Errorf("OpenAICompatAnthropicEffort(%q)=%q want %q", tc.level, got, tc.anthCompat) | |
| 32 | + | } | |
| 29 | 33 | if got := OpenAICodexReasoningEffort(tc.level); got != tc.codex { | |
| 30 | 34 | t.Errorf("OpenAICodexReasoningEffort(%q)=%q want %q", tc.level, got, tc.codex) | |
| 31 | 35 | } | |
| @@ -6,18 +6,19 @@ func TestReasoningEffortMappings(t *testing.T) { | |||||
| 6 | cases := []struct { | 6 | cases := []struct { | ||
| 7 | level string | 7 | level string | ||
| 8 | openai string | 8 | openai string | ||
| 9 | + | anthCompat string | |||
| 9 | codex string | 10 | codex string | ||
| 10 | budget int | 11 | budget int | ||
| 11 | normalized string | 12 | normalized string | ||
| 12 | }{ | 13 | }{ | ||
| 13 | - | {"off", "", "", 0, ""}, | 14 | + | {"off", "", "", "", 0, ""}, |
| 14 | - | {"minimum", "low", "low", 1024, "minimum"}, | 15 | + | {"minimum", "low", "low", "low", 1024, "minimum"}, |
| 15 | - | {"minimal", "low", "low", 1024, "minimum"}, | 16 | + | {"minimal", "low", "low", "low", 1024, "minimum"}, |
| 16 | - | {"low", "low", "low", 2048, "low"}, | 17 | + | {"low", "low", "low", "low", 2048, "low"}, |
| 17 | - | {"medium", "medium", "medium", 8192, "medium"}, | 18 | + | {"medium", "medium", "medium", "medium", 8192, "medium"}, |
| 18 | - | {"high", "high", "high", 16384, "high"}, | 19 | + | {"high", "high", "high", "high", 16384, "high"}, |
| 19 | - | {"maximum", "high", "xhigh", 32768, "maximum"}, | 20 | + | {"maximum", "high", "xhigh", "xhigh", 32768, "maximum"}, |
| 20 | - | {"max", "high", "xhigh", 32768, "maximum"}, | 21 | + | {"max", "high", "xhigh", "xhigh", 32768, "maximum"}, |
| 21 | } | 22 | } | ||
| 22 | for _, tc := range cases { | 23 | for _, tc := range cases { | ||
| 23 | if got := NormalizeReasoning(tc.level); got != tc.normalized { | 24 | if got := NormalizeReasoning(tc.level); got != tc.normalized { | ||
| @@ -26,6 +27,9 @@ func TestReasoningEffortMappings(t *testing.T) { | |||||
| 26 | if got := OpenAIReasoningEffort(tc.level); got != tc.openai { | 27 | if got := OpenAIReasoningEffort(tc.level); got != tc.openai { | ||
| 27 | t.Errorf("OpenAIReasoningEffort(%q)=%q want %q", tc.level, got, tc.openai) | 28 | t.Errorf("OpenAIReasoningEffort(%q)=%q want %q", tc.level, got, tc.openai) | ||
| 28 | } | 29 | } | ||
| 30 | + | if got := OpenAICompatAnthropicEffort(tc.level); got != tc.anthCompat { | |||
| 31 | + | t.Errorf("OpenAICompatAnthropicEffort(%q)=%q want %q", tc.level, got, tc.anthCompat) | |||
| 32 | + | } | |||
| 29 | if got := OpenAICodexReasoningEffort(tc.level); got != tc.codex { | 33 | if got := OpenAICodexReasoningEffort(tc.level); got != tc.codex { | ||
| 30 | t.Errorf("OpenAICodexReasoningEffort(%q)=%q want %q", tc.level, got, tc.codex) | 34 | t.Errorf("OpenAICodexReasoningEffort(%q)=%q want %q", tc.level, got, tc.codex) | ||
| 31 | } | 35 | } | ||