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
This commit is contained in:
Luis Pater
2026-06-18 22:38:45 +08:00
parent 62c4b377dd
commit ac8fb9706f
4 changed files with 83 additions and 2 deletions

View File

@@ -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))

View File

@@ -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))

View File

@@ -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))

View File

@@ -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: &registry.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: &registry.ThinkingSupport{Min: 1, Max: 65535, Levels: []string{"minimal", "low", "medium", "high"}, ZeroAllowed: true, DynamicAllowed: true},
},
{
ID: "claude-budget-model",
Object: "model",