From 5afc0f1d5e9ed8d47809a1bd1f54834bc7e75375 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Sat, 4 Jul 2026 00:36:40 +0800 Subject: [PATCH] fix(translator): remove temperature parameter handling in Claude request transformations Closes: #4071 --- internal/runtime/executor/claude_executor.go | 15 +++---- .../runtime/executor/claude_executor_test.go | 40 +++++++++---------- .../claude/gemini/claude_gemini_request.go | 7 +--- .../gemini/claude_gemini_request_test.go | 24 +++++++++++ .../chat-completions/claude_openai_request.go | 9 ++--- .../claude_openai_request_test.go | 21 ++++++++++ 6 files changed, 76 insertions(+), 40 deletions(-) diff --git a/internal/runtime/executor/claude_executor.go b/internal/runtime/executor/claude_executor.go index 5ece6fbd..83150d16 100644 --- a/internal/runtime/executor/claude_executor.go +++ b/internal/runtime/executor/claude_executor.go @@ -244,7 +244,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 = normalizeClaudeSamplingForThinking(body) + body = normalizeClaudeSamplingForUpstream(body) // Auto-inject cache_control if missing (optimization for ClawdBot/clients without caching support) if countCacheControls(body) == 0 { @@ -434,7 +434,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 = normalizeClaudeSamplingForThinking(body) + body = normalizeClaudeSamplingForUpstream(body) // Auto-inject cache_control if missing (optimization for ClawdBot/clients without caching support) if countCacheControls(body) == 0 { @@ -865,16 +865,13 @@ func disableThinkingIfToolChoiceForced(body []byte) []byte { return body } -// normalizeClaudeSamplingForThinking keeps Anthropic message requests valid when -// thinking is active. Anthropic rejects non-default sampling while thinking is -// enabled/adaptive/auto. -func normalizeClaudeSamplingForThinking(body []byte) []byte { +// normalizeClaudeSamplingForUpstream keeps Anthropic message requests valid. +func normalizeClaudeSamplingForUpstream(body []byte) []byte { + body, _ = sjson.DeleteBytes(body, "temperature") + 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) { - body, _ = sjson.SetBytes(body, "temperature", 1) - } body, _ = sjson.DeleteBytes(body, "top_p") body, _ = sjson.DeleteBytes(body, "top_k") } diff --git a/internal/runtime/executor/claude_executor_test.go b/internal/runtime/executor/claude_executor_test.go index c78923df..6d654b20 100644 --- a/internal/runtime/executor/claude_executor_test.go +++ b/internal/runtime/executor/claude_executor_test.go @@ -2659,30 +2659,30 @@ func TestApplyCloaking_PreservesConfiguredStrictModeAndSensitiveWordsWhenModeOmi } } -func TestNormalizeClaudeSamplingForThinking_AdaptiveCoercesTemperatureToOne(t *testing.T) { +func TestNormalizeClaudeSamplingForUpstream_RemovesTemperature(t *testing.T) { payload := []byte(`{"temperature":0,"thinking":{"type":"adaptive"},"output_config":{"effort":"max"}}`) - out := normalizeClaudeSamplingForThinking(payload) + out := normalizeClaudeSamplingForUpstream(payload) - if got := gjson.GetBytes(out, "temperature").Float(); got != 1 { - t.Fatalf("temperature = %v, want 1", got) + if gjson.GetBytes(out, "temperature").Exists() { + t.Fatalf("temperature should be removed") } } -func TestNormalizeClaudeSamplingForThinking_EnabledCoercesTemperatureToOne(t *testing.T) { +func TestNormalizeClaudeSamplingForUpstream_RemovesTemperatureWithThinkingEnabled(t *testing.T) { payload := []byte(`{"temperature":0.2,"thinking":{"type":"enabled","budget_tokens":2048}}`) - out := normalizeClaudeSamplingForThinking(payload) + out := normalizeClaudeSamplingForUpstream(payload) - if got := gjson.GetBytes(out, "temperature").Float(); got != 1 { - t.Fatalf("temperature = %v, want 1", got) + if gjson.GetBytes(out, "temperature").Exists() { + t.Fatalf("temperature should be removed") } } -func TestNormalizeClaudeSamplingForThinking_RemovesTopPAndTopK(t *testing.T) { +func TestNormalizeClaudeSamplingForUpstream_RemovesTopPAndTopKForThinking(t *testing.T) { payload := []byte(`{"temperature":0.2,"top_p":0.9,"top_k":40,"thinking":{"type":"adaptive"}}`) - out := normalizeClaudeSamplingForThinking(payload) + out := normalizeClaudeSamplingForUpstream(payload) - if got := gjson.GetBytes(out, "temperature").Float(); got != 1 { - t.Fatalf("temperature = %v, want 1", got) + if gjson.GetBytes(out, "temperature").Exists() { + t.Fatalf("temperature should be removed") } if gjson.GetBytes(out, "top_p").Exists() { t.Fatalf("top_p should be removed when thinking is active") @@ -2692,12 +2692,12 @@ func TestNormalizeClaudeSamplingForThinking_RemovesTopPAndTopK(t *testing.T) { } } -func TestNormalizeClaudeSamplingForThinking_NoThinkingLeavesTemperatureAlone(t *testing.T) { +func TestNormalizeClaudeSamplingForUpstream_NoThinkingRemovesOnlyTemperature(t *testing.T) { payload := []byte(`{"temperature":0,"top_p":0.9,"top_k":40,"messages":[{"role":"user","content":"hi"}]}`) - out := normalizeClaudeSamplingForThinking(payload) + out := normalizeClaudeSamplingForUpstream(payload) - if got := gjson.GetBytes(out, "temperature").Float(); got != 0 { - t.Fatalf("temperature = %v, want 0", got) + if gjson.GetBytes(out, "temperature").Exists() { + t.Fatalf("temperature should be removed") } if got := gjson.GetBytes(out, "top_p").Float(); got != 0.9 { t.Fatalf("top_p = %v, want 0.9", got) @@ -2707,16 +2707,16 @@ func TestNormalizeClaudeSamplingForThinking_NoThinkingLeavesTemperatureAlone(t * } } -func TestNormalizeClaudeSamplingForThinking_AfterForcedToolChoiceKeepsOriginalTemperature(t *testing.T) { +func TestNormalizeClaudeSamplingForUpstream_AfterForcedToolChoiceRemovesTemperature(t *testing.T) { payload := []byte(`{"temperature":0,"thinking":{"type":"adaptive"},"output_config":{"effort":"max"},"tool_choice":{"type":"any"}}`) out := disableThinkingIfToolChoiceForced(payload) - out = normalizeClaudeSamplingForThinking(out) + out = normalizeClaudeSamplingForUpstream(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) + if gjson.GetBytes(out, "temperature").Exists() { + t.Fatalf("temperature should be removed") } } diff --git a/internal/translator/claude/gemini/claude_gemini_request.go b/internal/translator/claude/gemini/claude_gemini_request.go index 1f5bf8ed..bd9a3447 100644 --- a/internal/translator/claude/gemini/claude_gemini_request.go +++ b/internal/translator/claude/gemini/claude_gemini_request.go @@ -114,11 +114,8 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream if maxTokens := genConfig.Get("maxOutputTokens"); maxTokens.Exists() { out, _ = sjson.SetBytes(out, "max_tokens", maxTokens.Int()) } - // Temperature setting for controlling response randomness - if temp := genConfig.Get("temperature"); temp.Exists() { - out, _ = sjson.SetBytes(out, "temperature", temp.Float()) - } else if topP := genConfig.Get("topP"); topP.Exists() { - // Top P setting for nucleus sampling (filtered out if temperature is set) + // Top P setting for nucleus sampling. + if topP := genConfig.Get("topP"); topP.Exists() { out, _ = sjson.SetBytes(out, "top_p", topP.Float()) } // Stop sequences configuration for custom termination conditions diff --git a/internal/translator/claude/gemini/claude_gemini_request_test.go b/internal/translator/claude/gemini/claude_gemini_request_test.go index 06224d5a..e599bb0c 100644 --- a/internal/translator/claude/gemini/claude_gemini_request_test.go +++ b/internal/translator/claude/gemini/claude_gemini_request_test.go @@ -61,3 +61,27 @@ func TestConvertGeminiRequestToClaude_PreservesCustomToolIDs(t *testing.T) { }) } } + +func TestConvertGeminiRequestToClaude_DropsTemperature(t *testing.T) { + raw := []byte(`{ + "generationConfig": { + "temperature": 0.2, + "topP": 0.8 + }, + "contents": [ + { + "role": "user", + "parts": [{"text": "hi"}] + } + ] + }`) + + out := ConvertGeminiRequestToClaude("claude-sonnet-5", raw, false) + + if gjson.GetBytes(out, "temperature").Exists() { + t.Fatalf("temperature should be removed") + } + if got := gjson.GetBytes(out, "top_p").Float(); got != 0.8 { + t.Fatalf("top_p = %v, want 0.8", got) + } +} diff --git a/internal/translator/claude/openai/chat-completions/claude_openai_request.go b/internal/translator/claude/openai/chat-completions/claude_openai_request.go index b4df9b54..606ad2bb 100644 --- a/internal/translator/claude/openai/chat-completions/claude_openai_request.go +++ b/internal/translator/claude/openai/chat-completions/claude_openai_request.go @@ -31,7 +31,7 @@ var ( // It extracts the model name, system instruction, message contents, and tool declarations // from the raw JSON request and returns them in the format expected by the Claude Code API. // The function performs comprehensive transformation including: -// 1. Model name mapping and parameter extraction (max_tokens, temperature, top_p, etc.) +// 1. Model name mapping and parameter extraction (max_tokens, top_p, etc.) // 2. Message content conversion from OpenAI to Claude Code format // 3. Tool call and tool result handling with proper ID mapping // 4. Image data conversion from OpenAI data URLs to Claude Code base64 format @@ -136,11 +136,8 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream out, _ = sjson.SetBytes(out, "max_tokens", maxTokens.Int()) } - // Temperature setting for controlling response randomness - if temp := root.Get("temperature"); temp.Exists() { - out, _ = sjson.SetBytes(out, "temperature", temp.Float()) - } else if topP := root.Get("top_p"); topP.Exists() { - // Top P setting for nucleus sampling (filtered out if temperature is set) + // Top P setting for nucleus sampling. + if topP := root.Get("top_p"); topP.Exists() { out, _ = sjson.SetBytes(out, "top_p", topP.Float()) } diff --git a/internal/translator/claude/openai/chat-completions/claude_openai_request_test.go b/internal/translator/claude/openai/chat-completions/claude_openai_request_test.go index 8adf74fe..52c5b1f6 100644 --- a/internal/translator/claude/openai/chat-completions/claude_openai_request_test.go +++ b/internal/translator/claude/openai/chat-completions/claude_openai_request_test.go @@ -44,6 +44,27 @@ func TestConvertOpenAIRequestToClaude_SanitizesToolCallIDsForClaude(t *testing.T } } +func TestConvertOpenAIRequestToClaude_DropsTemperature(t *testing.T) { + inputJSON := `{ + "model": "gpt-4.1", + "temperature": 0.2, + "top_p": 0.8, + "messages": [ + {"role": "user", "content": "hi"} + ] + }` + + result := ConvertOpenAIRequestToClaude("claude-sonnet-5", []byte(inputJSON), false) + resultJSON := gjson.ParseBytes(result) + + if resultJSON.Get("temperature").Exists() { + t.Fatalf("temperature should be removed") + } + if got := resultJSON.Get("top_p").Float(); got != 0.8 { + t.Fatalf("top_p = %v, want 0.8", got) + } +} + func TestConvertOpenAIRequestToClaude_ToolResultTextAndBase64Image(t *testing.T) { inputJSON := `{ "model": "gpt-4.1", -- 2.51.2