Skip to content

Commit 5afc0f1

Browse files
committed
fix(translator): remove temperature parameter handling in Claude request transformations
Closes: #4071
1 parent 9e9c244 commit 5afc0f1

6 files changed

Lines changed: 76 additions & 40 deletions

File tree

internal/runtime/executor/claude_executor.go

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
244244

245245
// Disable thinking if tool_choice forces tool use (Anthropic API constraint)
246246
body = disableThinkingIfToolChoiceForced(body)
247-
body = normalizeClaudeSamplingForThinking(body)
247+
body = normalizeClaudeSamplingForUpstream(body)
248248

249249
// Auto-inject cache_control if missing (optimization for ClawdBot/clients without caching support)
250250
if countCacheControls(body) == 0 {
@@ -434,7 +434,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
434434

435435
// Disable thinking if tool_choice forces tool use (Anthropic API constraint)
436436
body = disableThinkingIfToolChoiceForced(body)
437-
body = normalizeClaudeSamplingForThinking(body)
437+
body = normalizeClaudeSamplingForUpstream(body)
438438

439439
// Auto-inject cache_control if missing (optimization for ClawdBot/clients without caching support)
440440
if countCacheControls(body) == 0 {
@@ -865,16 +865,13 @@ func disableThinkingIfToolChoiceForced(body []byte) []byte {
865865
return body
866866
}
867867

868-
// normalizeClaudeSamplingForThinking keeps Anthropic message requests valid when
869-
// thinking is active. Anthropic rejects non-default sampling while thinking is
870-
// enabled/adaptive/auto.
871-
func normalizeClaudeSamplingForThinking(body []byte) []byte {
868+
// normalizeClaudeSamplingForUpstream keeps Anthropic message requests valid.
869+
func normalizeClaudeSamplingForUpstream(body []byte) []byte {
870+
body, _ = sjson.DeleteBytes(body, "temperature")
871+
872872
thinkingType := strings.ToLower(strings.TrimSpace(gjson.GetBytes(body, "thinking.type").String()))
873873
switch thinkingType {
874874
case "enabled", "adaptive", "auto":
875-
if temp := gjson.GetBytes(body, "temperature"); temp.Exists() && (temp.Type != gjson.Number || temp.Float() != 1) {
876-
body, _ = sjson.SetBytes(body, "temperature", 1)
877-
}
878875
body, _ = sjson.DeleteBytes(body, "top_p")
879876
body, _ = sjson.DeleteBytes(body, "top_k")
880877
}

internal/runtime/executor/claude_executor_test.go

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2659,30 +2659,30 @@ func TestApplyCloaking_PreservesConfiguredStrictModeAndSensitiveWordsWhenModeOmi
26592659
}
26602660
}
26612661

2662-
func TestNormalizeClaudeSamplingForThinking_AdaptiveCoercesTemperatureToOne(t *testing.T) {
2662+
func TestNormalizeClaudeSamplingForUpstream_RemovesTemperature(t *testing.T) {
26632663
payload := []byte(`{"temperature":0,"thinking":{"type":"adaptive"},"output_config":{"effort":"max"}}`)
2664-
out := normalizeClaudeSamplingForThinking(payload)
2664+
out := normalizeClaudeSamplingForUpstream(payload)
26652665

2666-
if got := gjson.GetBytes(out, "temperature").Float(); got != 1 {
2667-
t.Fatalf("temperature = %v, want 1", got)
2666+
if gjson.GetBytes(out, "temperature").Exists() {
2667+
t.Fatalf("temperature should be removed")
26682668
}
26692669
}
26702670

2671-
func TestNormalizeClaudeSamplingForThinking_EnabledCoercesTemperatureToOne(t *testing.T) {
2671+
func TestNormalizeClaudeSamplingForUpstream_RemovesTemperatureWithThinkingEnabled(t *testing.T) {
26722672
payload := []byte(`{"temperature":0.2,"thinking":{"type":"enabled","budget_tokens":2048}}`)
2673-
out := normalizeClaudeSamplingForThinking(payload)
2673+
out := normalizeClaudeSamplingForUpstream(payload)
26742674

2675-
if got := gjson.GetBytes(out, "temperature").Float(); got != 1 {
2676-
t.Fatalf("temperature = %v, want 1", got)
2675+
if gjson.GetBytes(out, "temperature").Exists() {
2676+
t.Fatalf("temperature should be removed")
26772677
}
26782678
}
26792679

2680-
func TestNormalizeClaudeSamplingForThinking_RemovesTopPAndTopK(t *testing.T) {
2680+
func TestNormalizeClaudeSamplingForUpstream_RemovesTopPAndTopKForThinking(t *testing.T) {
26812681
payload := []byte(`{"temperature":0.2,"top_p":0.9,"top_k":40,"thinking":{"type":"adaptive"}}`)
2682-
out := normalizeClaudeSamplingForThinking(payload)
2682+
out := normalizeClaudeSamplingForUpstream(payload)
26832683

2684-
if got := gjson.GetBytes(out, "temperature").Float(); got != 1 {
2685-
t.Fatalf("temperature = %v, want 1", got)
2684+
if gjson.GetBytes(out, "temperature").Exists() {
2685+
t.Fatalf("temperature should be removed")
26862686
}
26872687
if gjson.GetBytes(out, "top_p").Exists() {
26882688
t.Fatalf("top_p should be removed when thinking is active")
@@ -2692,12 +2692,12 @@ func TestNormalizeClaudeSamplingForThinking_RemovesTopPAndTopK(t *testing.T) {
26922692
}
26932693
}
26942694

2695-
func TestNormalizeClaudeSamplingForThinking_NoThinkingLeavesTemperatureAlone(t *testing.T) {
2695+
func TestNormalizeClaudeSamplingForUpstream_NoThinkingRemovesOnlyTemperature(t *testing.T) {
26962696
payload := []byte(`{"temperature":0,"top_p":0.9,"top_k":40,"messages":[{"role":"user","content":"hi"}]}`)
2697-
out := normalizeClaudeSamplingForThinking(payload)
2697+
out := normalizeClaudeSamplingForUpstream(payload)
26982698

2699-
if got := gjson.GetBytes(out, "temperature").Float(); got != 0 {
2700-
t.Fatalf("temperature = %v, want 0", got)
2699+
if gjson.GetBytes(out, "temperature").Exists() {
2700+
t.Fatalf("temperature should be removed")
27012701
}
27022702
if got := gjson.GetBytes(out, "top_p").Float(); got != 0.9 {
27032703
t.Fatalf("top_p = %v, want 0.9", got)
@@ -2707,16 +2707,16 @@ func TestNormalizeClaudeSamplingForThinking_NoThinkingLeavesTemperatureAlone(t *
27072707
}
27082708
}
27092709

2710-
func TestNormalizeClaudeSamplingForThinking_AfterForcedToolChoiceKeepsOriginalTemperature(t *testing.T) {
2710+
func TestNormalizeClaudeSamplingForUpstream_AfterForcedToolChoiceRemovesTemperature(t *testing.T) {
27112711
payload := []byte(`{"temperature":0,"thinking":{"type":"adaptive"},"output_config":{"effort":"max"},"tool_choice":{"type":"any"}}`)
27122712
out := disableThinkingIfToolChoiceForced(payload)
2713-
out = normalizeClaudeSamplingForThinking(out)
2713+
out = normalizeClaudeSamplingForUpstream(out)
27142714

27152715
if gjson.GetBytes(out, "thinking").Exists() {
27162716
t.Fatalf("thinking should be removed when tool_choice forces tool use")
27172717
}
2718-
if got := gjson.GetBytes(out, "temperature").Float(); got != 0 {
2719-
t.Fatalf("temperature = %v, want 0", got)
2718+
if gjson.GetBytes(out, "temperature").Exists() {
2719+
t.Fatalf("temperature should be removed")
27202720
}
27212721
}
27222722

internal/translator/claude/gemini/claude_gemini_request.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,8 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
114114
if maxTokens := genConfig.Get("maxOutputTokens"); maxTokens.Exists() {
115115
out, _ = sjson.SetBytes(out, "max_tokens", maxTokens.Int())
116116
}
117-
// Temperature setting for controlling response randomness
118-
if temp := genConfig.Get("temperature"); temp.Exists() {
119-
out, _ = sjson.SetBytes(out, "temperature", temp.Float())
120-
} else if topP := genConfig.Get("topP"); topP.Exists() {
121-
// Top P setting for nucleus sampling (filtered out if temperature is set)
117+
// Top P setting for nucleus sampling.
118+
if topP := genConfig.Get("topP"); topP.Exists() {
122119
out, _ = sjson.SetBytes(out, "top_p", topP.Float())
123120
}
124121
// Stop sequences configuration for custom termination conditions

internal/translator/claude/gemini/claude_gemini_request_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,27 @@ func TestConvertGeminiRequestToClaude_PreservesCustomToolIDs(t *testing.T) {
6161
})
6262
}
6363
}
64+
65+
func TestConvertGeminiRequestToClaude_DropsTemperature(t *testing.T) {
66+
raw := []byte(`{
67+
"generationConfig": {
68+
"temperature": 0.2,
69+
"topP": 0.8
70+
},
71+
"contents": [
72+
{
73+
"role": "user",
74+
"parts": [{"text": "hi"}]
75+
}
76+
]
77+
}`)
78+
79+
out := ConvertGeminiRequestToClaude("claude-sonnet-5", raw, false)
80+
81+
if gjson.GetBytes(out, "temperature").Exists() {
82+
t.Fatalf("temperature should be removed")
83+
}
84+
if got := gjson.GetBytes(out, "top_p").Float(); got != 0.8 {
85+
t.Fatalf("top_p = %v, want 0.8", got)
86+
}
87+
}

internal/translator/claude/openai/chat-completions/claude_openai_request.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ var (
3131
// It extracts the model name, system instruction, message contents, and tool declarations
3232
// from the raw JSON request and returns them in the format expected by the Claude Code API.
3333
// The function performs comprehensive transformation including:
34-
// 1. Model name mapping and parameter extraction (max_tokens, temperature, top_p, etc.)
34+
// 1. Model name mapping and parameter extraction (max_tokens, top_p, etc.)
3535
// 2. Message content conversion from OpenAI to Claude Code format
3636
// 3. Tool call and tool result handling with proper ID mapping
3737
// 4. Image data conversion from OpenAI data URLs to Claude Code base64 format
@@ -136,11 +136,8 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
136136
out, _ = sjson.SetBytes(out, "max_tokens", maxTokens.Int())
137137
}
138138

139-
// Temperature setting for controlling response randomness
140-
if temp := root.Get("temperature"); temp.Exists() {
141-
out, _ = sjson.SetBytes(out, "temperature", temp.Float())
142-
} else if topP := root.Get("top_p"); topP.Exists() {
143-
// Top P setting for nucleus sampling (filtered out if temperature is set)
139+
// Top P setting for nucleus sampling.
140+
if topP := root.Get("top_p"); topP.Exists() {
144141
out, _ = sjson.SetBytes(out, "top_p", topP.Float())
145142
}
146143

internal/translator/claude/openai/chat-completions/claude_openai_request_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,27 @@ func TestConvertOpenAIRequestToClaude_SanitizesToolCallIDsForClaude(t *testing.T
4444
}
4545
}
4646

47+
func TestConvertOpenAIRequestToClaude_DropsTemperature(t *testing.T) {
48+
inputJSON := `{
49+
"model": "gpt-4.1",
50+
"temperature": 0.2,
51+
"top_p": 0.8,
52+
"messages": [
53+
{"role": "user", "content": "hi"}
54+
]
55+
}`
56+
57+
result := ConvertOpenAIRequestToClaude("claude-sonnet-5", []byte(inputJSON), false)
58+
resultJSON := gjson.ParseBytes(result)
59+
60+
if resultJSON.Get("temperature").Exists() {
61+
t.Fatalf("temperature should be removed")
62+
}
63+
if got := resultJSON.Get("top_p").Float(); got != 0.8 {
64+
t.Fatalf("top_p = %v, want 0.8", got)
65+
}
66+
}
67+
4768
func TestConvertOpenAIRequestToClaude_ToolResultTextAndBase64Image(t *testing.T) {
4869
inputJSON := `{
4970
"model": "gpt-4.1",

0 commit comments

Comments
 (0)