From 8abc16ee88b9b1bb674f35c20bf8cf813b057808 Mon Sep 17 00:00:00 2001 From: Ojus M Save Date: Thu, 4 Jun 2026 11:18:07 -0700 Subject: [PATCH] fix(providers): omit temperature for Claude Opus/Sonnet 4.6+ (#1180) Anthropic returns HTTP 400 when temperature is sent to claude-opus-4-6, claude-sonnet-4-6, and newer 4.x models. Agent summoning and chat both hardcode temperature; skip the field for affected model IDs. Co-authored-by: Ojus Save --- internal/providers/adapter_anthropic_test.go | 42 ++++++++++++++++++++ internal/providers/anthropic_request.go | 28 ++++++++++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/internal/providers/adapter_anthropic_test.go b/internal/providers/adapter_anthropic_test.go index da976683..88d4769c 100644 --- a/internal/providers/adapter_anthropic_test.go +++ b/internal/providers/adapter_anthropic_test.go @@ -150,6 +150,48 @@ func TestAnthropicAdapterToRequest_Thinking(t *testing.T) { } } +func TestAnthropicAdapterToRequest_SkipsTemperatureForClaude46(t *testing.T) { + adapter, _ := NewAnthropicAdapter(ProviderConfig{APIKey: "sk-test"}) + + for _, model := range []string{"claude-opus-4-6", "claude-sonnet-4-6", "claude-opus-4-7-20260501"} { + t.Run(model, func(t *testing.T) { + req := ChatRequest{ + Model: model, + Messages: []Message{{Role: "user", Content: "hi"}}, + Options: map[string]any{OptTemperature: 0.7}, + } + data, _, err := adapter.ToRequest(req) + if err != nil { + t.Fatal(err) + } + var body map[string]any + if err := json.Unmarshal(data, &body); err != nil { + t.Fatal(err) + } + if _, hasTemp := body["temperature"]; hasTemp { + t.Errorf("model %q: temperature should be omitted", model) + } + }) + } + + req := ChatRequest{ + Model: "claude-sonnet-4-5-20250929", + Messages: []Message{{Role: "user", Content: "hi"}}, + Options: map[string]any{OptTemperature: 0.7}, + } + data, _, err := adapter.ToRequest(req) + if err != nil { + t.Fatal(err) + } + var body map[string]any + if err := json.Unmarshal(data, &body); err != nil { + t.Fatal(err) + } + if body["temperature"] != 0.7 { + t.Errorf("sonnet 4.5 should keep temperature, got %v", body["temperature"]) + } +} + func TestAnthropicAdapterFromResponse_ToolCalls(t *testing.T) { adapter, _ := NewAnthropicAdapter(ProviderConfig{APIKey: "sk-test"}) diff --git a/internal/providers/anthropic_request.go b/internal/providers/anthropic_request.go index 58dfb3a0..306eb158 100644 --- a/internal/providers/anthropic_request.go +++ b/internal/providers/anthropic_request.go @@ -213,7 +213,9 @@ func (p *AnthropicProvider) buildRequestBody(model string, req ChatRequest, stre body["max_tokens"] = v } if v, ok := req.Options[OptTemperature]; ok { - body["temperature"] = v + if !anthropicSkipsTemperature(model) { + body["temperature"] = v + } } // Enable extended thinking if thinking_level is set @@ -234,6 +236,30 @@ func (p *AnthropicProvider) buildRequestBody(model string, req ChatRequest, stre return body } +// anthropicSkipsTemperature reports whether the Messages API rejects sampling +// parameters for this model. Claude Opus/Sonnet 4.6+ and Opus 4.7+ return HTTP +// 400 when temperature (and top_p/top_k) are included; omit them entirely. +func anthropicSkipsTemperature(model string) bool { + m := strings.ToLower(model) + for _, family := range []string{"claude-opus-4-", "claude-sonnet-4-"} { + after, ok := strings.CutPrefix(m, family) + if !ok { + continue + } + minor := 0 + for _, c := range after { + if c < '0' || c > '9' { + break + } + minor = minor*10 + int(c-'0') + } + if minor >= 6 { + return true + } + } + return false +} + // anthropicThinkingBudget maps a thinking level to a token budget. func anthropicThinkingBudget(level string) int { switch level {