Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions internal/providers/adapter_anthropic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"})

Expand Down
28 changes: 27 additions & 1 deletion internal/providers/anthropic_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down