diff --git a/internal/thinking/provider/antigravity/apply.go b/internal/thinking/provider/antigravity/apply.go index a55f808d..7d5a5075 100644 --- a/internal/thinking/provider/antigravity/apply.go +++ b/internal/thinking/provider/antigravity/apply.go @@ -116,7 +116,16 @@ func (a *Applier) applyLevelFormat(body []byte, config thinking.ThinkingConfig) level := string(config.Level) result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.thinkingLevel", level) - result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.includeThoughts", true) + + // Respect user's explicit includeThoughts setting from original body; default to true if not set + // Support both camelCase and snake_case variants + includeThoughts := true + if inc := gjson.GetBytes(body, "request.generationConfig.thinkingConfig.includeThoughts"); inc.Exists() { + includeThoughts = inc.Bool() + } else if inc := gjson.GetBytes(body, "request.generationConfig.thinkingConfig.include_thoughts"); inc.Exists() { + includeThoughts = inc.Bool() + } + result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.includeThoughts", includeThoughts) return result, nil } @@ -129,14 +138,29 @@ func (a *Applier) applyBudgetFormat(body []byte, config thinking.ThinkingConfig, result, _ = sjson.DeleteBytes(result, "request.generationConfig.thinkingConfig.include_thoughts") budget := config.Budget - includeThoughts := false - switch config.Mode { - case thinking.ModeNone: - includeThoughts = false - case thinking.ModeAuto: - includeThoughts = true - default: - includeThoughts = budget > 0 + + // Determine includeThoughts: respect user's explicit setting from original body if provided + // Support both camelCase and snake_case variants + var includeThoughts bool + var userSetIncludeThoughts bool + if inc := gjson.GetBytes(body, "request.generationConfig.thinkingConfig.includeThoughts"); inc.Exists() { + includeThoughts = inc.Bool() + userSetIncludeThoughts = true + } else if inc := gjson.GetBytes(body, "request.generationConfig.thinkingConfig.include_thoughts"); inc.Exists() { + includeThoughts = inc.Bool() + userSetIncludeThoughts = true + } + + if !userSetIncludeThoughts { + // No explicit setting, use default logic based on mode + switch config.Mode { + case thinking.ModeNone: + includeThoughts = false + case thinking.ModeAuto: + includeThoughts = true + default: + includeThoughts = budget > 0 + } } // Apply Claude-specific constraints diff --git a/internal/thinking/provider/gemini/apply.go b/internal/thinking/provider/gemini/apply.go index 2c06a75a..39399c09 100644 --- a/internal/thinking/provider/gemini/apply.go +++ b/internal/thinking/provider/gemini/apply.go @@ -140,7 +140,16 @@ func (a *Applier) applyLevelFormat(body []byte, config thinking.ThinkingConfig) level := string(config.Level) result, _ = sjson.SetBytes(result, "generationConfig.thinkingConfig.thinkingLevel", level) - result, _ = sjson.SetBytes(result, "generationConfig.thinkingConfig.includeThoughts", true) + + // Respect user's explicit includeThoughts setting from original body; default to true if not set + // Support both camelCase and snake_case variants + includeThoughts := true + if inc := gjson.GetBytes(body, "generationConfig.thinkingConfig.includeThoughts"); inc.Exists() { + includeThoughts = inc.Bool() + } else if inc := gjson.GetBytes(body, "generationConfig.thinkingConfig.include_thoughts"); inc.Exists() { + includeThoughts = inc.Bool() + } + result, _ = sjson.SetBytes(result, "generationConfig.thinkingConfig.includeThoughts", includeThoughts) return result, nil } @@ -153,18 +162,33 @@ func (a *Applier) applyBudgetFormat(body []byte, config thinking.ThinkingConfig) result, _ = sjson.DeleteBytes(result, "generationConfig.thinkingConfig.include_thoughts") budget := config.Budget - // ModeNone semantics: - // - ModeNone + Budget=0: completely disable thinking - // - ModeNone + Budget>0: forced to think but hide output (includeThoughts=false) - // When ZeroAllowed=false, ValidateConfig clamps Budget to Min while preserving ModeNone. - includeThoughts := false - switch config.Mode { - case thinking.ModeNone: - includeThoughts = false - case thinking.ModeAuto: - includeThoughts = true - default: - includeThoughts = budget > 0 + + // Determine includeThoughts: respect user's explicit setting from original body if provided + // Support both camelCase and snake_case variants + var includeThoughts bool + var userSetIncludeThoughts bool + if inc := gjson.GetBytes(body, "generationConfig.thinkingConfig.includeThoughts"); inc.Exists() { + includeThoughts = inc.Bool() + userSetIncludeThoughts = true + } else if inc := gjson.GetBytes(body, "generationConfig.thinkingConfig.include_thoughts"); inc.Exists() { + includeThoughts = inc.Bool() + userSetIncludeThoughts = true + } + + if !userSetIncludeThoughts { + // No explicit setting, use default logic based on mode + // ModeNone semantics: + // - ModeNone + Budget=0: completely disable thinking + // - ModeNone + Budget>0: forced to think but hide output (includeThoughts=false) + // When ZeroAllowed=false, ValidateConfig clamps Budget to Min while preserving ModeNone. + switch config.Mode { + case thinking.ModeNone: + includeThoughts = false + case thinking.ModeAuto: + includeThoughts = true + default: + includeThoughts = budget > 0 + } } result, _ = sjson.SetBytes(result, "generationConfig.thinkingConfig.thinkingBudget", budget) diff --git a/internal/thinking/provider/geminicli/apply.go b/internal/thinking/provider/geminicli/apply.go index f60c94a9..476e5b6d 100644 --- a/internal/thinking/provider/geminicli/apply.go +++ b/internal/thinking/provider/geminicli/apply.go @@ -101,7 +101,16 @@ func (a *Applier) applyLevelFormat(body []byte, config thinking.ThinkingConfig) level := string(config.Level) result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.thinkingLevel", level) - result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.includeThoughts", true) + + // Respect user's explicit includeThoughts setting from original body; default to true if not set + // Support both camelCase and snake_case variants + includeThoughts := true + if inc := gjson.GetBytes(body, "request.generationConfig.thinkingConfig.includeThoughts"); inc.Exists() { + includeThoughts = inc.Bool() + } else if inc := gjson.GetBytes(body, "request.generationConfig.thinkingConfig.include_thoughts"); inc.Exists() { + includeThoughts = inc.Bool() + } + result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.includeThoughts", includeThoughts) return result, nil } @@ -114,14 +123,29 @@ func (a *Applier) applyBudgetFormat(body []byte, config thinking.ThinkingConfig) result, _ = sjson.DeleteBytes(result, "request.generationConfig.thinkingConfig.include_thoughts") budget := config.Budget - includeThoughts := false - switch config.Mode { - case thinking.ModeNone: - includeThoughts = false - case thinking.ModeAuto: - includeThoughts = true - default: - includeThoughts = budget > 0 + + // Determine includeThoughts: respect user's explicit setting from original body if provided + // Support both camelCase and snake_case variants + var includeThoughts bool + var userSetIncludeThoughts bool + if inc := gjson.GetBytes(body, "request.generationConfig.thinkingConfig.includeThoughts"); inc.Exists() { + includeThoughts = inc.Bool() + userSetIncludeThoughts = true + } else if inc := gjson.GetBytes(body, "request.generationConfig.thinkingConfig.include_thoughts"); inc.Exists() { + includeThoughts = inc.Bool() + userSetIncludeThoughts = true + } + + if !userSetIncludeThoughts { + // No explicit setting, use default logic based on mode + switch config.Mode { + case thinking.ModeNone: + includeThoughts = false + case thinking.ModeAuto: + includeThoughts = true + default: + includeThoughts = budget > 0 + } } result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.thinkingBudget", budget) diff --git a/test/thinking_conversion_test.go b/test/thinking_conversion_test.go index 83a0e139..fc20199e 100644 --- a/test/thinking_conversion_test.go +++ b/test/thinking_conversion_test.go @@ -1441,28 +1441,6 @@ func TestThinkingE2EMatrix_Body(t *testing.T) { expectValue: "medium", expectErr: false, }, - // Case 9001: thinking_budget=64000 (snake_case) → high (Gemini -> Codex) - { - name: "9001", - from: "gemini", - to: "codex", - model: "level-model", - inputJSON: `{"model":"level-model","contents":[{"role":"user","parts":[{"text":"hi"}]}],"generationConfig":{"thinkingConfig":{"thinking_budget":64000}}}`, - expectField: "reasoning.effort", - expectValue: "high", - expectErr: false, - }, - // Case 9002: thinking_level=high (snake_case) → reasoning_effort=high (Gemini -> OpenAI) - { - name: "9002", - from: "gemini", - to: "openai", - model: "level-model", - inputJSON: `{"model":"level-model","contents":[{"role":"user","parts":[{"text":"hi"}]}],"generationConfig":{"thinkingConfig":{"thinking_level":"high"}}}`, - expectField: "reasoning_effort", - expectValue: "high", - expectErr: false, - }, // Case 11: Claude no param → passthrough (no thinking) { name: "11", @@ -1473,17 +1451,6 @@ func TestThinkingE2EMatrix_Body(t *testing.T) { expectField: "", expectErr: false, }, - // Case 9003: thinking_budget=8192 (snake_case) → thinking.budget_tokens=8192 (Gemini -> Claude) - { - name: "9003", - from: "gemini", - to: "claude", - model: "level-model", - inputJSON: `{"model":"level-model","contents":[{"role":"user","parts":[{"text":"hi"}]}],"generationConfig":{"thinkingConfig":{"thinking_budget":8192}}}`, - expectField: "thinking.budget_tokens", - expectValue: "8192", - expectErr: false, - }, // Case 12: thinking.budget_tokens=8192 → medium { name: "12", @@ -1557,19 +1524,6 @@ func TestThinkingE2EMatrix_Body(t *testing.T) { // gemini-budget-model (Min=128, Max=20000, ZeroAllowed=false, DynamicAllowed=true) - // Case 9004: thinking_budget=8192 (snake_case) → passthrough+normalize to thinkingBudget (Gemini -> Gemini) - { - name: "9004", - from: "gemini", - to: "gemini", - model: "gemini-budget-model", - inputJSON: `{"model":"gemini-budget-model","contents":[{"role":"user","parts":[{"text":"hi"}]}],"generationConfig":{"thinkingConfig":{"thinking_budget":8192}}}`, - expectField: "generationConfig.thinkingConfig.thinkingBudget", - expectValue: "8192", - includeThoughts: "true", - expectErr: false, - }, - // Case 18: No param → passthrough { name: "18",