From ac8fb9706fb84bedfbd1f813738680fdc6767115 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Thu, 18 Jun 2026 22:38:45 +0800 Subject: [PATCH] feat(thinking): remove `thinkingConfig` for `ModeNone` with zero budget and no level - Updated Gemini, Gemini CLI, and Antigravity logic to delete `thinkingConfig` when `ModeNone` is set, `Budget=0`, and `Level` is empty. - Adjusted tests to validate this behavior across multiple scenarios and models with zero-allowed configurations. - Extended test cases for additional coverage of mixed-model behavior. Closes: #3138 --- .../thinking/provider/antigravity/apply.go | 4 ++ internal/thinking/provider/gemini/apply.go | 8 ++- internal/thinking/provider/geminicli/apply.go | 4 ++ test/thinking_conversion_test.go | 69 +++++++++++++++++++ 4 files changed, 83 insertions(+), 2 deletions(-) diff --git a/internal/thinking/provider/antigravity/apply.go b/internal/thinking/provider/antigravity/apply.go index 0a8f1c453..4a2c76c30 100644 --- a/internal/thinking/provider/antigravity/apply.go +++ b/internal/thinking/provider/antigravity/apply.go @@ -102,6 +102,10 @@ func (a *Applier) applyLevelFormat(body []byte, config thinking.ThinkingConfig) result, _ = sjson.DeleteBytes(result, "request.generationConfig.thinkingConfig.include_thoughts") if config.Mode == thinking.ModeNone { + if config.Budget == 0 && config.Level == "" { + result, _ = sjson.DeleteBytes(result, "request.generationConfig.thinkingConfig") + return result, nil + } result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.includeThoughts", false) if config.Level != "" { result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.thinkingLevel", string(config.Level)) diff --git a/internal/thinking/provider/gemini/apply.go b/internal/thinking/provider/gemini/apply.go index 8e6e83f33..92a8d7ec7 100644 --- a/internal/thinking/provider/gemini/apply.go +++ b/internal/thinking/provider/gemini/apply.go @@ -22,7 +22,7 @@ import ( // // Gemini-specific behavior: // - Gemini 2.5: thinkingBudget format, flash series supports ZeroAllowed -// - Gemini 3.x: thinkingLevel format, cannot be disabled +// - Gemini 3.x: thinkingLevel format, disable by removing thinkingConfig when zero is allowed // - Use ThinkingSupport.Levels to decide output format type Applier struct{} @@ -114,7 +114,7 @@ func (a *Applier) applyCompatible(body []byte, config thinking.ThinkingConfig) ( func (a *Applier) applyLevelFormat(body []byte, config thinking.ThinkingConfig) ([]byte, error) { // ModeNone semantics: - // - ModeNone + Budget=0: completely disable thinking (not possible for Level-only models) + // - ModeNone + Budget=0: remove thinkingConfig to disable thinking // - ModeNone + Budget>0: forced to think but hide output (includeThoughts=false) // ValidateConfig sets config.Level to the lowest level when ModeNone + Budget > 0. @@ -126,6 +126,10 @@ func (a *Applier) applyLevelFormat(body []byte, config thinking.ThinkingConfig) result, _ = sjson.DeleteBytes(result, "generationConfig.thinkingConfig.include_thoughts") if config.Mode == thinking.ModeNone { + if config.Budget == 0 && config.Level == "" { + result, _ = sjson.DeleteBytes(result, "generationConfig.thinkingConfig") + return result, nil + } result, _ = sjson.SetBytes(result, "generationConfig.thinkingConfig.includeThoughts", false) if config.Level != "" { result, _ = sjson.SetBytes(result, "generationConfig.thinkingConfig.thinkingLevel", string(config.Level)) diff --git a/internal/thinking/provider/geminicli/apply.go b/internal/thinking/provider/geminicli/apply.go index e9311e8c1..1bc9315f0 100644 --- a/internal/thinking/provider/geminicli/apply.go +++ b/internal/thinking/provider/geminicli/apply.go @@ -87,6 +87,10 @@ func (a *Applier) applyLevelFormat(body []byte, config thinking.ThinkingConfig) result, _ = sjson.DeleteBytes(result, "request.generationConfig.thinkingConfig.include_thoughts") if config.Mode == thinking.ModeNone { + if config.Budget == 0 && config.Level == "" { + result, _ = sjson.DeleteBytes(result, "request.generationConfig.thinkingConfig") + return result, nil + } result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.includeThoughts", false) if config.Level != "" { result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.thinkingLevel", string(config.Level)) diff --git a/test/thinking_conversion_test.go b/test/thinking_conversion_test.go index 430eb9250..8e000be6c 100644 --- a/test/thinking_conversion_test.go +++ b/test/thinking_conversion_test.go @@ -1515,6 +1515,66 @@ func TestThinkingE2EMatrix_Body(t *testing.T) { includeThoughts: "false", expectErr: false, }, + // Case 31A: reasoning_effort=none with zero allowed → delete thinkingConfig + { + name: "31A", + from: "openai", + to: "gemini", + model: "gemini-zero-mixed-model", + inputJSON: `{"model":"gemini-zero-mixed-model","messages":[{"role":"user","content":"hi"}],"reasoning_effort":"none"}`, + expectField: "", + expectErr: false, + }, + // Case 31B: reasoning_effort=none with zero allowed to Gemini CLI → delete thinkingConfig + { + name: "31B", + from: "openai", + to: "gemini-cli", + model: "gemini-zero-mixed-model", + inputJSON: `{"model":"gemini-zero-mixed-model","messages":[{"role":"user","content":"hi"}],"reasoning_effort":"none"}`, + expectField: "", + expectErr: false, + }, + // Case 31C: reasoning_effort=none with zero allowed to Antigravity → delete thinkingConfig + { + name: "31C", + from: "openai", + to: "antigravity", + model: "gemini-zero-mixed-model", + inputJSON: `{"model":"gemini-zero-mixed-model","messages":[{"role":"user","content":"hi"}],"reasoning_effort":"none"}`, + expectField: "", + expectErr: false, + }, + // Case 31D: reasoning.effort=none with zero allowed → delete thinkingConfig + { + name: "31D", + from: "openai-response", + to: "gemini", + model: "gemini-zero-mixed-model", + inputJSON: `{"model":"gemini-zero-mixed-model","input":[{"role":"user","content":"hi"}],"reasoning":{"effort":"none"}}`, + expectField: "", + expectErr: false, + }, + // Case 31E: reasoning.effort=none with zero allowed to Gemini CLI → delete thinkingConfig + { + name: "31E", + from: "openai-response", + to: "gemini-cli", + model: "gemini-zero-mixed-model", + inputJSON: `{"model":"gemini-zero-mixed-model","input":[{"role":"user","content":"hi"}],"reasoning":{"effort":"none"}}`, + expectField: "", + expectErr: false, + }, + // Case 31F: reasoning.effort=none with zero allowed to Antigravity → delete thinkingConfig + { + name: "31F", + from: "openai-response", + to: "antigravity", + model: "gemini-zero-mixed-model", + inputJSON: `{"model":"gemini-zero-mixed-model","input":[{"role":"user","content":"hi"}],"reasoning":{"effort":"none"}}`, + expectField: "", + expectErr: false, + }, // Case 32: reasoning_effort=auto → -1 (DynamicAllowed=true) { name: "32", @@ -2957,6 +3017,15 @@ func getTestModels() []*registry.ModelInfo { DisplayName: "Gemini Mixed Model", Thinking: ®istry.ThinkingSupport{Min: 128, Max: 32768, Levels: []string{"low", "high"}, ZeroAllowed: false, DynamicAllowed: true}, }, + { + ID: "gemini-zero-mixed-model", + Object: "model", + Created: 1700000000, + OwnedBy: "test", + Type: "gemini", + DisplayName: "Gemini Zero Mixed Model", + Thinking: ®istry.ThinkingSupport{Min: 1, Max: 65535, Levels: []string{"minimal", "low", "medium", "high"}, ZeroAllowed: true, DynamicAllowed: true}, + }, { ID: "claude-budget-model", Object: "model",