diff --git a/internal/runtime/executor/claude_executor.go b/internal/runtime/executor/claude_executor.go index 56c2c5400..7b2e5d8d5 100644 --- a/internal/runtime/executor/claude_executor.go +++ b/internal/runtime/executor/claude_executor.go @@ -137,6 +137,7 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r // Disable thinking if tool_choice forces tool use (Anthropic API constraint) body = disableThinkingIfToolChoiceForced(body) + body = normalizeClaudeTemperatureForThinking(body) // Auto-inject cache_control if missing (optimization for ClawdBot/clients without caching support) if countCacheControls(body) == 0 { @@ -307,6 +308,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A // Disable thinking if tool_choice forces tool use (Anthropic API constraint) body = disableThinkingIfToolChoiceForced(body) + body = normalizeClaudeTemperatureForThinking(body) // Auto-inject cache_control if missing (optimization for ClawdBot/clients without caching support) if countCacheControls(body) == 0 { @@ -651,6 +653,25 @@ func disableThinkingIfToolChoiceForced(body []byte) []byte { return body } +// normalizeClaudeTemperatureForThinking keeps Anthropic message requests valid when +// thinking is enabled. Anthropic rejects temperatures other than 1 when +// thinking.type is enabled/adaptive/auto. +func normalizeClaudeTemperatureForThinking(body []byte) []byte { + if !gjson.GetBytes(body, "temperature").Exists() { + return body + } + + thinkingType := strings.ToLower(strings.TrimSpace(gjson.GetBytes(body, "thinking.type").String())) + switch thinkingType { + case "enabled", "adaptive", "auto": + if temp := gjson.GetBytes(body, "temperature"); temp.Exists() && temp.Type == gjson.Number && temp.Float() == 1 { + return body + } + body, _ = sjson.SetBytes(body, "temperature", 1) + } + return body +} + type compositeReadCloser struct { io.Reader closers []func() error diff --git a/internal/runtime/executor/claude_executor_test.go b/internal/runtime/executor/claude_executor_test.go index 89bab2aac..74cec0a35 100644 --- a/internal/runtime/executor/claude_executor_test.go +++ b/internal/runtime/executor/claude_executor_test.go @@ -1833,3 +1833,43 @@ func TestApplyCloaking_PreservesConfiguredStrictModeAndSensitiveWordsWhenModeOmi t.Fatalf("expected configured sensitive word obfuscation to apply, got %q", got) } } + +func TestNormalizeClaudeTemperatureForThinking_AdaptiveCoercesToOne(t *testing.T) { + payload := []byte(`{"temperature":0,"thinking":{"type":"adaptive"},"output_config":{"effort":"max"}}`) + out := normalizeClaudeTemperatureForThinking(payload) + + if got := gjson.GetBytes(out, "temperature").Float(); got != 1 { + t.Fatalf("temperature = %v, want 1", got) + } +} + +func TestNormalizeClaudeTemperatureForThinking_EnabledCoercesToOne(t *testing.T) { + payload := []byte(`{"temperature":0.2,"thinking":{"type":"enabled","budget_tokens":2048}}`) + out := normalizeClaudeTemperatureForThinking(payload) + + if got := gjson.GetBytes(out, "temperature").Float(); got != 1 { + t.Fatalf("temperature = %v, want 1", got) + } +} + +func TestNormalizeClaudeTemperatureForThinking_NoThinkingLeavesTemperatureAlone(t *testing.T) { + payload := []byte(`{"temperature":0,"messages":[{"role":"user","content":"hi"}]}`) + out := normalizeClaudeTemperatureForThinking(payload) + + if got := gjson.GetBytes(out, "temperature").Float(); got != 0 { + t.Fatalf("temperature = %v, want 0", got) + } +} + +func TestNormalizeClaudeTemperatureForThinking_AfterForcedToolChoiceKeepsOriginalTemperature(t *testing.T) { + payload := []byte(`{"temperature":0,"thinking":{"type":"adaptive"},"output_config":{"effort":"max"},"tool_choice":{"type":"any"}}`) + out := disableThinkingIfToolChoiceForced(payload) + out = normalizeClaudeTemperatureForThinking(out) + + if gjson.GetBytes(out, "thinking").Exists() { + t.Fatalf("thinking should be removed when tool_choice forces tool use") + } + if got := gjson.GetBytes(out, "temperature").Float(); got != 0 { + t.Fatalf("temperature = %v, want 0", got) + } +}