From 0910c4d7d5063c7033c2272ff3ba8f5864a0d3c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Jan 2026 14:09:53 +0000 Subject: [PATCH 1/3] fix: ensure MaxTokens >= budget when extended thinking is enabled When extended thinking is enabled via the reasoning parameter, Anthropic requires max_tokens to be at least the thinking budget_tokens. This fix ensures MaxTokens is bumped to the budget value when it's lower, in both ChatRequest and ResponsesRequest conversion paths. --- internal/providers/anthropic/anthropic.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/internal/providers/anthropic/anthropic.go b/internal/providers/anthropic/anthropic.go index 9991c4fc..58fd1075 100644 --- a/internal/providers/anthropic/anthropic.go +++ b/internal/providers/anthropic/anthropic.go @@ -185,9 +185,14 @@ func convertToAnthropicRequest(req *core.ChatRequest) *anthropicRequest { // Map reasoning effort to Anthropic extended thinking if req.Reasoning != nil && req.Reasoning.Effort != "" { + budget := reasoningEffortToBudgetTokens(req.Reasoning.Effort) anthropicReq.Thinking = &anthropicThinking{ Type: "enabled", - BudgetTokens: reasoningEffortToBudgetTokens(req.Reasoning.Effort), + BudgetTokens: budget, + } + // Ensure MaxTokens is at least the budget tokens + if anthropicReq.MaxTokens < budget { + anthropicReq.MaxTokens = budget } // Extended thinking requires temperature to be unset (defaults to 1) if anthropicReq.Temperature != nil { @@ -509,9 +514,14 @@ func convertResponsesRequestToAnthropic(req *core.ResponsesRequest) *anthropicRe // Map reasoning effort to Anthropic extended thinking if req.Reasoning != nil && req.Reasoning.Effort != "" { + budget := reasoningEffortToBudgetTokens(req.Reasoning.Effort) anthropicReq.Thinking = &anthropicThinking{ Type: "enabled", - BudgetTokens: reasoningEffortToBudgetTokens(req.Reasoning.Effort), + BudgetTokens: budget, + } + // Ensure MaxTokens is at least the budget tokens + if anthropicReq.MaxTokens < budget { + anthropicReq.MaxTokens = budget } // Extended thinking requires temperature to be unset (defaults to 1) if anthropicReq.Temperature != nil { From e8558d0c1aae29db111dfa509304d56f23147a2c Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:32:54 +0100 Subject: [PATCH 2/3] Add logging for MaxTokens adjustment in Anthropic extended thinking (#50) * Initial plan * feat: add logging for MaxTokens adjustment in Anthropic provider Add informative log statements when MaxTokens is adjusted to meet Anthropic extended thinking requirements. Logs include original value, adjusted value, and the reason for adjustment. Co-authored-by: SantiagoDePolonia <16936376+SantiagoDePolonia@users.noreply.github.com> * refactor: extract MaxTokens logging into helper function Extract duplicate logging statements into a dedicated helper function to improve code maintainability and ensure consistency across both ChatCompletion and StreamChatCompletion flows. Co-authored-by: SantiagoDePolonia <16936376+SantiagoDePolonia@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: SantiagoDePolonia <16936376+SantiagoDePolonia@users.noreply.github.com> --- internal/providers/anthropic/anthropic.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/internal/providers/anthropic/anthropic.go b/internal/providers/anthropic/anthropic.go index 58fd1075..b3f13d1e 100644 --- a/internal/providers/anthropic/anthropic.go +++ b/internal/providers/anthropic/anthropic.go @@ -169,6 +169,14 @@ func reasoningEffortToBudgetTokens(effort string) int { } } +// logMaxTokensAdjustment logs when MaxTokens is adjusted to meet Anthropic requirements +func logMaxTokensAdjustment(original, adjusted int, reason string) { + slog.Info("MaxTokens adjusted to meet Anthropic extended thinking requirements", + "original", original, + "adjusted", adjusted, + "reason", reason) +} + // convertToAnthropicRequest converts core.ChatRequest to Anthropic format func convertToAnthropicRequest(req *core.ChatRequest) *anthropicRequest { anthropicReq := &anthropicRequest{ @@ -192,6 +200,8 @@ func convertToAnthropicRequest(req *core.ChatRequest) *anthropicRequest { } // Ensure MaxTokens is at least the budget tokens if anthropicReq.MaxTokens < budget { + logMaxTokensAdjustment(anthropicReq.MaxTokens, budget, + "extended thinking budget_tokens must be <= max_tokens") anthropicReq.MaxTokens = budget } // Extended thinking requires temperature to be unset (defaults to 1) @@ -521,6 +531,8 @@ func convertResponsesRequestToAnthropic(req *core.ResponsesRequest) *anthropicRe } // Ensure MaxTokens is at least the budget tokens if anthropicReq.MaxTokens < budget { + logMaxTokensAdjustment(anthropicReq.MaxTokens, budget, + "extended thinking budget_tokens must be <= max_tokens") anthropicReq.MaxTokens = budget } // Extended thinking requires temperature to be unset (defaults to 1) From 14eca867c2d92f9ee2ad369afaa62464a5d06826 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:33:07 +0100 Subject: [PATCH 3/3] Add comprehensive tests for reasoning effort edge case handling (#49) * Initial plan * test: add comprehensive tests for reasoning effort edge cases Co-authored-by: SantiagoDePolonia <16936376+SantiagoDePolonia@users.noreply.github.com> * fix: improve test code quality based on review feedback Co-authored-by: SantiagoDePolonia <16936376+SantiagoDePolonia@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: SantiagoDePolonia <16936376+SantiagoDePolonia@users.noreply.github.com> --- .../providers/anthropic/anthropic_test.go | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) diff --git a/internal/providers/anthropic/anthropic_test.go b/internal/providers/anthropic/anthropic_test.go index 92fbc2e5..fccbf9ce 100644 --- a/internal/providers/anthropic/anthropic_test.go +++ b/internal/providers/anthropic/anthropic_test.go @@ -1189,3 +1189,219 @@ func TestConvertAnthropicResponseToResponses(t *testing.T) { t.Errorf("TotalTokens = %d, want 30", result.Usage.TotalTokens) } } + +// TestConvertToAnthropicRequest_ReasoningEffort tests handling of reasoning effort parameter +func TestConvertToAnthropicRequest_ReasoningEffort(t *testing.T) { + tests := []struct { +name string +reasoning *core.Reasoning +maxTokens *int +expectedThinking bool +expectedBudget int +expectedMaxTokens int +expectedTemperature *float64 +}{ +{ +name: "reasoning nil - no thinking enabled", +reasoning: nil, +maxTokens: intPtr(1000), +expectedThinking: false, +expectedMaxTokens: 1000, +expectedTemperature: nil, +}, +{ +name: "reasoning with empty effort - no thinking enabled", +reasoning: &core.Reasoning{Effort: ""}, +maxTokens: intPtr(1000), +expectedThinking: false, +expectedMaxTokens: 1000, +expectedTemperature: nil, +}, +{ +name: "reasoning with low effort", +reasoning: &core.Reasoning{Effort: "low"}, +maxTokens: intPtr(10000), +expectedThinking: true, +expectedBudget: 5000, +expectedMaxTokens: 10000, +expectedTemperature: nil, // Should be unset for reasoning +}, +{ +name: "reasoning with medium effort", +reasoning: &core.Reasoning{Effort: "medium"}, +maxTokens: intPtr(15000), +expectedThinking: true, +expectedBudget: 10000, +expectedMaxTokens: 15000, +expectedTemperature: nil, +}, +{ +name: "reasoning with high effort", +reasoning: &core.Reasoning{Effort: "high"}, +maxTokens: intPtr(25000), +expectedThinking: true, +expectedBudget: 20000, +expectedMaxTokens: 25000, +expectedTemperature: nil, +}, +{ +name: "reasoning with invalid effort - defaults to low", +reasoning: &core.Reasoning{Effort: "invalid"}, +maxTokens: intPtr(10000), +expectedThinking: true, +expectedBudget: 5000, // Defaults to low +expectedMaxTokens: 10000, +expectedTemperature: nil, +}, +{ +name: "reasoning bumps up max_tokens when too low", +reasoning: &core.Reasoning{Effort: "high"}, +maxTokens: intPtr(1000), // Lower than budget +expectedThinking: true, +expectedBudget: 20000, +expectedMaxTokens: 20000, // Should be bumped to budget +expectedTemperature: nil, +}, +{ +name: "reasoning removes temperature when set", +reasoning: &core.Reasoning{Effort: "medium"}, +maxTokens: intPtr(15000), +expectedThinking: true, +expectedBudget: 10000, +expectedMaxTokens: 15000, +expectedTemperature: nil, // Should be removed even if originally set +}, +} + +for _, tt := range tests { +t.Run(tt.name, func(t *testing.T) { +req := &core.ChatRequest{ +Model: "claude-3-5-sonnet-20241022", +Messages: []core.Message{{Role: "user", Content: "test"}}, +MaxTokens: tt.maxTokens, +Reasoning: tt.reasoning, +} + +// Set temperature for the test case about temperature removal +if tt.name == "reasoning removes temperature when set" { +temp := 0.7 +req.Temperature = &temp +} + +result := convertToAnthropicRequest(req) + +// Check thinking field +if tt.expectedThinking { +if result.Thinking == nil { +t.Error("Thinking should be enabled but is nil") +} else { +if result.Thinking.Type != "enabled" { +t.Errorf("Thinking.Type = %q, want %q", result.Thinking.Type, "enabled") +} +if result.Thinking.BudgetTokens != tt.expectedBudget { +t.Errorf("Thinking.BudgetTokens = %d, want %d", result.Thinking.BudgetTokens, tt.expectedBudget) +} +} +} else { +if result.Thinking != nil { +t.Errorf("Thinking should be nil but got %+v", result.Thinking) +} +} + +// Check max_tokens +if result.MaxTokens != tt.expectedMaxTokens { +t.Errorf("MaxTokens = %d, want %d", result.MaxTokens, tt.expectedMaxTokens) +} + +// Check temperature +if result.Temperature != tt.expectedTemperature { +if result.Temperature == nil && tt.expectedTemperature != nil { +t.Errorf("Temperature should be %v but is nil", *tt.expectedTemperature) +} else if result.Temperature != nil && tt.expectedTemperature == nil { +t.Errorf("Temperature should be nil but is %v", *result.Temperature) +} else if result.Temperature != nil && tt.expectedTemperature != nil && *result.Temperature != *tt.expectedTemperature { +t.Errorf("Temperature = %v, want %v", *result.Temperature, *tt.expectedTemperature) +} +} +}) +} +} + +// TestConvertResponsesRequestToAnthropic_ReasoningEffort tests reasoning in Responses API +func TestConvertResponsesRequestToAnthropic_ReasoningEffort(t *testing.T) { + tests := []struct { +name string +reasoning *core.Reasoning +maxOutputTokens *int +expectedThinking bool +expectedBudget int +expectedMaxTokens int +}{ +{ +name: "no reasoning", +reasoning: nil, +maxOutputTokens: intPtr(1000), +expectedThinking: false, +expectedMaxTokens: 1000, +}, +{ +name: "empty effort", +reasoning: &core.Reasoning{Effort: ""}, +maxOutputTokens: intPtr(1000), +expectedThinking: false, +expectedMaxTokens: 1000, +}, +{ +name: "low effort bumps max tokens", +reasoning: &core.Reasoning{Effort: "low"}, +maxOutputTokens: intPtr(1000), +expectedThinking: true, +expectedBudget: 5000, +expectedMaxTokens: 5000, // Bumped from 1000 +}, +{ +name: "high effort with sufficient tokens", +reasoning: &core.Reasoning{Effort: "high"}, +maxOutputTokens: intPtr(25000), +expectedThinking: true, +expectedBudget: 20000, +expectedMaxTokens: 25000, +}, +} + +for _, tt := range tests { +t.Run(tt.name, func(t *testing.T) { +req := &core.ResponsesRequest{ +Model: "claude-3-5-sonnet-20241022", +Input: "test input", +MaxOutputTokens: tt.maxOutputTokens, +Reasoning: tt.reasoning, +} + +result := convertResponsesRequestToAnthropic(req) + +if tt.expectedThinking { +if result.Thinking == nil { +t.Error("Thinking should be enabled but is nil") +} else { +if result.Thinking.BudgetTokens != tt.expectedBudget { +t.Errorf("Thinking.BudgetTokens = %d, want %d", result.Thinking.BudgetTokens, tt.expectedBudget) +} +} +} else { +if result.Thinking != nil { +t.Errorf("Thinking should be nil but got %+v", result.Thinking) +} +} + +if result.MaxTokens != tt.expectedMaxTokens { +t.Errorf("MaxTokens = %d, want %d", result.MaxTokens, tt.expectedMaxTokens) +} +}) +} +} + +// Helper function to create int pointer +func intPtr(i int) *int { +return &i +}