From ce87714ef11fb9e083e3ff0a6d3f76fd944dec22 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:10:47 +0800 Subject: [PATCH] feat(thinking): normalize effort levels in adaptive thinking requests to prevent validation errors --- .../claude/gemini/claude_gemini_request.go | 22 ++++++++++--------- .../chat-completions/claude_openai_request.go | 3 ++- .../claude_openai-responses_request.go | 3 ++- .../codex/claude/codex_claude_request.go | 11 +++------- .../claude/gemini-cli_claude_request.go | 19 ++++++++++++---- .../gemini/claude/gemini_claude_request.go | 10 +++------ .../openai/claude/openai_claude_request.go | 11 +++------- 7 files changed, 40 insertions(+), 39 deletions(-) diff --git a/internal/translator/claude/gemini/claude_gemini_request.go b/internal/translator/claude/gemini/claude_gemini_request.go index 66914462..a8d97b9d 100644 --- a/internal/translator/claude/gemini/claude_gemini_request.go +++ b/internal/translator/claude/gemini/claude_gemini_request.go @@ -120,6 +120,8 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream supportsAdaptive := mi != nil && mi.Thinking != nil && len(mi.Thinking.Levels) > 0 supportsMax := supportsAdaptive && thinking.HasLevel(mi.Thinking.Levels, string(thinking.LevelMax)) + // MapToClaudeEffort normalizes levels (e.g. minimal→low, xhigh→high) to avoid + // validation errors since validate treats same-provider unsupported levels as errors. thinkingLevel := thinkingConfig.Get("thinkingLevel") if !thinkingLevel.Exists() { thinkingLevel = thinkingConfig.Get("thinking_level") @@ -134,12 +136,12 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream out, _ = sjson.Delete(out, "thinking.budget_tokens") out, _ = sjson.Delete(out, "output_config.effort") default: - effort, ok := thinking.MapToClaudeEffort(level, supportsMax) - if ok { - out, _ = sjson.Set(out, "thinking.type", "adaptive") - out, _ = sjson.Delete(out, "thinking.budget_tokens") - out, _ = sjson.Set(out, "output_config.effort", effort) + if mapped, ok := thinking.MapToClaudeEffort(level, supportsMax); ok { + level = mapped } + out, _ = sjson.Set(out, "thinking.type", "adaptive") + out, _ = sjson.Delete(out, "thinking.budget_tokens") + out, _ = sjson.Set(out, "output_config.effort", level) } } else { switch level { @@ -173,12 +175,12 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream default: level, ok := thinking.ConvertBudgetToLevel(budget) if ok { - effort, ok := thinking.MapToClaudeEffort(level, supportsMax) - if ok { - out, _ = sjson.Set(out, "thinking.type", "adaptive") - out, _ = sjson.Delete(out, "thinking.budget_tokens") - out, _ = sjson.Set(out, "output_config.effort", effort) + if mapped, okM := thinking.MapToClaudeEffort(level, supportsMax); okM { + level = mapped } + out, _ = sjson.Set(out, "thinking.type", "adaptive") + out, _ = sjson.Delete(out, "thinking.budget_tokens") + out, _ = sjson.Set(out, "output_config.effort", level) } } } else { 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 2706a73e..1b88bb0e 100644 --- a/internal/translator/claude/openai/chat-completions/claude_openai_request.go +++ b/internal/translator/claude/openai/chat-completions/claude_openai_request.go @@ -74,6 +74,8 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream supportsMax := supportsAdaptive && thinking.HasLevel(mi.Thinking.Levels, string(thinking.LevelMax)) // Claude 4.6 supports adaptive thinking with output_config.effort. + // MapToClaudeEffort normalizes levels (e.g. minimal→low, xhigh→high) to avoid + // validation errors since validate treats same-provider unsupported levels as errors. if supportsAdaptive { switch effort { case "none": @@ -85,7 +87,6 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream out, _ = sjson.Delete(out, "thinking.budget_tokens") out, _ = sjson.Delete(out, "output_config.effort") default: - // Map non-Claude effort levels into Claude 4.6 effort vocabulary. if mapped, ok := thinking.MapToClaudeEffort(effort, supportsMax); ok { effort = mapped } diff --git a/internal/translator/claude/openai/responses/claude_openai-responses_request.go b/internal/translator/claude/openai/responses/claude_openai-responses_request.go index 9e8f28da..cb550b09 100644 --- a/internal/translator/claude/openai/responses/claude_openai-responses_request.go +++ b/internal/translator/claude/openai/responses/claude_openai-responses_request.go @@ -62,6 +62,8 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte supportsMax := supportsAdaptive && thinking.HasLevel(mi.Thinking.Levels, string(thinking.LevelMax)) // Claude 4.6 supports adaptive thinking with output_config.effort. + // MapToClaudeEffort normalizes levels (e.g. minimal→low, xhigh→high) to avoid + // validation errors since validate treats same-provider unsupported levels as errors. if supportsAdaptive { switch effort { case "none": @@ -73,7 +75,6 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte out, _ = sjson.Delete(out, "thinking.budget_tokens") out, _ = sjson.Delete(out, "output_config.effort") default: - // Map non-Claude effort levels into Claude 4.6 effort vocabulary. if mapped, ok := thinking.MapToClaudeEffort(effort, supportsMax); ok { effort = mapped } diff --git a/internal/translator/codex/claude/codex_claude_request.go b/internal/translator/codex/claude/codex_claude_request.go index 7846400e..a635aba8 100644 --- a/internal/translator/codex/claude/codex_claude_request.go +++ b/internal/translator/codex/claude/codex_claude_request.go @@ -232,19 +232,14 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) } case "adaptive", "auto": // Adaptive thinking can carry an explicit effort in output_config.effort (Claude 4.6). - // Preserve it when present; otherwise keep the previous "max capacity" sentinel. + // Pass through directly; ApplyThinking handles clamping to target model's levels. effort := "" if v := rootResult.Get("output_config.effort"); v.Exists() && v.Type == gjson.String { effort = strings.ToLower(strings.TrimSpace(v.String())) } - switch effort { - case "minimal", "low", "medium", "high": + if effort != "" { reasoningEffort = effort - case "max": - reasoningEffort = string(thinking.LevelXHigh) - default: - // Keep adaptive/auto as a high level sentinel; ApplyThinking resolves it - // to model-specific max capability. + } else { reasoningEffort = string(thinking.LevelXHigh) } case "disabled": diff --git a/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go b/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go index 653bbeb2..3f8921dc 100644 --- a/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go +++ b/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go @@ -171,7 +171,8 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) [] } } - // Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when type==enabled + // Map Anthropic thinking -> Gemini CLI thinkingConfig when enabled + // Translator only does format conversion, ApplyThinking handles model capability validation. if t := gjson.GetBytes(rawJSON, "thinking"); t.Exists() && t.IsObject() { switch t.Get("type").String() { case "enabled": @@ -181,9 +182,19 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) [] out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true) } case "adaptive", "auto": - // Keep adaptive/auto as a high level sentinel; ApplyThinking resolves it - // to model-specific max capability. - out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingLevel", "high") + // For adaptive thinking: + // - If output_config.effort is explicitly present, pass through as thinkingLevel. + // - Otherwise, treat it as "enabled with target-model maximum" and emit high. + // ApplyThinking handles clamping to target model's supported levels. + effort := "" + if v := gjson.GetBytes(rawJSON, "output_config.effort"); v.Exists() && v.Type == gjson.String { + effort = strings.ToLower(strings.TrimSpace(v.String())) + } + if effort != "" { + out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingLevel", effort) + } else { + out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingLevel", "high") + } out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true) } } diff --git a/internal/translator/gemini/claude/gemini_claude_request.go b/internal/translator/gemini/claude/gemini_claude_request.go index 7eed1cc7..172884bd 100644 --- a/internal/translator/gemini/claude/gemini_claude_request.go +++ b/internal/translator/gemini/claude/gemini_claude_request.go @@ -164,19 +164,15 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) } case "adaptive", "auto": // For adaptive thinking: - // - If output_config.effort is explicitly present, map it to thinkingLevel. + // - If output_config.effort is explicitly present, pass through as thinkingLevel. // - Otherwise, treat it as "enabled with target-model maximum" and emit thinkingBudget=max. + // ApplyThinking handles clamping to target model's supported levels. effort := "" if v := gjson.GetBytes(rawJSON, "output_config.effort"); v.Exists() && v.Type == gjson.String { effort = strings.ToLower(strings.TrimSpace(v.String())) } if effort != "" { - level := effort - switch level { - case "xhigh", "max": - level = "high" - } - out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingLevel", level) + out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingLevel", effort) } else { maxBudget := 0 if mi := registry.LookupModelInfo(modelName, "gemini"); mi != nil && mi.Thinking != nil { diff --git a/internal/translator/openai/claude/openai_claude_request.go b/internal/translator/openai/claude/openai_claude_request.go index 4d0f1a1d..ff46a830 100644 --- a/internal/translator/openai/claude/openai_claude_request.go +++ b/internal/translator/openai/claude/openai_claude_request.go @@ -77,19 +77,14 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream } case "adaptive", "auto": // Adaptive thinking can carry an explicit effort in output_config.effort (Claude 4.6). - // Preserve it when present; otherwise keep the previous "max capacity" sentinel. + // Pass through directly; ApplyThinking handles clamping to target model's levels. effort := "" if v := root.Get("output_config.effort"); v.Exists() && v.Type == gjson.String { effort = strings.ToLower(strings.TrimSpace(v.String())) } - switch effort { - case "minimal", "low", "medium", "high": + if effort != "" { out, _ = sjson.Set(out, "reasoning_effort", effort) - case "max": - out, _ = sjson.Set(out, "reasoning_effort", string(thinking.LevelXHigh)) - default: - // Keep adaptive/auto as a high level sentinel; ApplyThinking resolves it - // to model-specific max capability. + } else { out, _ = sjson.Set(out, "reasoning_effort", string(thinking.LevelXHigh)) } case "disabled":