fix(translator): remove temperature parameter handling in Claude request transformations

Closes: #4071
This commit is contained in:
Luis Pater
2026-07-04 00:36:58 +08:00
parent 9e9c244250
commit 5afc0f1d5e
6 changed files with 76 additions and 40 deletions
+6 -9
View File
@@ -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")
}
@@ -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")
}
}
@@ -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
@@ -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)
}
}
@@ -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())
}
@@ -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",