From 20390628451cc9e26dade50f820a59a36e2a4a9a Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Fri, 19 Dec 2025 22:07:43 +0800 Subject: [PATCH] fix(gemini): add optional skip for gemini3 thinking conversion --- .../runtime/executor/aistudio_executor.go | 4 +- internal/util/gemini_thinking.go | 59 ++++++------------- internal/util/thinking.go | 28 +++++++++ 3 files changed, 49 insertions(+), 42 deletions(-) diff --git a/internal/runtime/executor/aistudio_executor.go b/internal/runtime/executor/aistudio_executor.go index cac23c87..17c8170f 100644 --- a/internal/runtime/executor/aistudio_executor.go +++ b/internal/runtime/executor/aistudio_executor.go @@ -325,8 +325,8 @@ func (e *AIStudioExecutor) translateRequest(req cliproxyexecutor.Request, opts c payload = ApplyThinkingMetadata(payload, req.Metadata, req.Model) payload = util.ApplyGemini3ThinkingLevelFromMetadata(req.Model, req.Metadata, payload) payload = util.ApplyDefaultThinkingIfNeeded(req.Model, payload) - payload = util.ConvertThinkingLevelToBudget(payload, req.Model) - payload = util.NormalizeGeminiThinkingBudget(req.Model, payload) + payload = util.ConvertThinkingLevelToBudget(payload, req.Model, true) + payload = util.NormalizeGeminiThinkingBudget(req.Model, payload, true) payload = util.StripThinkingConfigIfUnsupported(req.Model, payload) payload = fixGeminiImageAspectRatio(req.Model, payload) payload = applyPayloadConfig(e.cfg, req.Model, payload) diff --git a/internal/util/gemini_thinking.go b/internal/util/gemini_thinking.go index 5a5a29a9..ba9e13ef 100644 --- a/internal/util/gemini_thinking.go +++ b/internal/util/gemini_thinking.go @@ -352,8 +352,9 @@ func StripThinkingConfigIfUnsupported(model string, body []byte) []byte { // NormalizeGeminiThinkingBudget normalizes the thinkingBudget value in a standard Gemini // request body (generationConfig.thinkingConfig.thinkingBudget path). -// For Gemini 3 models, converts thinkingBudget to thinkingLevel per Google's documentation. -func NormalizeGeminiThinkingBudget(model string, body []byte) []byte { +// For Gemini 3 models, converts thinkingBudget to thinkingLevel per Google's documentation, +// unless skipGemini3Check is provided and true. +func NormalizeGeminiThinkingBudget(model string, body []byte, skipGemini3Check ...bool) []byte { const budgetPath = "generationConfig.thinkingConfig.thinkingBudget" const levelPath = "generationConfig.thinkingConfig.thinkingLevel" @@ -363,7 +364,8 @@ func NormalizeGeminiThinkingBudget(model string, body []byte) []byte { } // For Gemini 3 models, convert thinkingBudget to thinkingLevel - if IsGemini3Model(model) { + skipGemini3 := len(skipGemini3Check) > 0 && skipGemini3Check[0] + if IsGemini3Model(model) && !skipGemini3 { if level, ok := ThinkingBudgetToGemini3Level(model, int(budget.Int())); ok { updated, _ := sjson.SetBytes(body, levelPath, level) updated, _ = sjson.DeleteBytes(updated, budgetPath) @@ -382,8 +384,9 @@ func NormalizeGeminiThinkingBudget(model string, body []byte) []byte { // NormalizeGeminiCLIThinkingBudget normalizes the thinkingBudget value in a Gemini CLI // request body (request.generationConfig.thinkingConfig.thinkingBudget path). -// For Gemini 3 models, converts thinkingBudget to thinkingLevel per Google's documentation. -func NormalizeGeminiCLIThinkingBudget(model string, body []byte) []byte { +// For Gemini 3 models, converts thinkingBudget to thinkingLevel per Google's documentation, +// unless skipGemini3Check is provided and true. +func NormalizeGeminiCLIThinkingBudget(model string, body []byte, skipGemini3Check ...bool) []byte { const budgetPath = "request.generationConfig.thinkingConfig.thinkingBudget" const levelPath = "request.generationConfig.thinkingConfig.thinkingLevel" @@ -393,7 +396,8 @@ func NormalizeGeminiCLIThinkingBudget(model string, body []byte) []byte { } // For Gemini 3 models, convert thinkingBudget to thinkingLevel - if IsGemini3Model(model) { + skipGemini3 := len(skipGemini3Check) > 0 && skipGemini3Check[0] + if IsGemini3Model(model) && !skipGemini3 { if level, ok := ThinkingBudgetToGemini3Level(model, int(budget.Int())); ok { updated, _ := sjson.SetBytes(body, levelPath, level) updated, _ = sjson.DeleteBytes(updated, budgetPath) @@ -477,7 +481,7 @@ func ApplyReasoningEffortToGeminiCLI(body []byte, effort string) []byte { // ConvertThinkingLevelToBudget checks for "generationConfig.thinkingConfig.thinkingLevel" // and converts it to "thinkingBudget" for Gemini 2.5 models. -// For Gemini 3 models, preserves thinkingLevel as-is (does not convert). +// For Gemini 3 models, preserves thinkingLevel unless skipGemini3Check is provided and true. // Mappings for Gemini 2.5: // - "high" -> 32768 // - "medium" -> 8192 @@ -485,43 +489,31 @@ func ApplyReasoningEffortToGeminiCLI(body []byte, effort string) []byte { // - "minimal" -> 512 // // It removes "thinkingLevel" after conversion (for Gemini 2.5 only). -func ConvertThinkingLevelToBudget(body []byte, model string) []byte { +func ConvertThinkingLevelToBudget(body []byte, model string, skipGemini3Check ...bool) []byte { levelPath := "generationConfig.thinkingConfig.thinkingLevel" res := gjson.GetBytes(body, levelPath) if !res.Exists() { return body } - // For Gemini 3 models, preserve thinkingLevel - don't convert to budget - if IsGemini3Model(model) { + // For Gemini 3 models, preserve thinkingLevel unless explicitly skipped + skipGemini3 := len(skipGemini3Check) > 0 && skipGemini3Check[0] + if IsGemini3Model(model) && !skipGemini3 { return body } - level := strings.ToLower(res.String()) - var budget int - switch level { - case "high": - budget = 32768 - case "medium": - budget = 8192 - case "low": - budget = 1024 - case "minimal": - budget = 512 - default: - // Unknown level - remove it and let the API use defaults + budget, ok := ThinkingLevelToBudget(res.String()) + if !ok { updated, _ := sjson.DeleteBytes(body, levelPath) return updated } - // Set budget budgetPath := "generationConfig.thinkingConfig.thinkingBudget" updated, err := sjson.SetBytes(body, budgetPath, budget) if err != nil { return body } - // Remove level updated, err = sjson.DeleteBytes(updated, levelPath) if err != nil { return body @@ -544,31 +536,18 @@ func ConvertThinkingLevelToBudgetCLI(body []byte, model string) []byte { return body } - level := strings.ToLower(res.String()) - var budget int - switch level { - case "high": - budget = 32768 - case "medium": - budget = 8192 - case "low": - budget = 1024 - case "minimal": - budget = 512 - default: - // Unknown level - remove it and let the API use defaults + budget, ok := ThinkingLevelToBudget(res.String()) + if !ok { updated, _ := sjson.DeleteBytes(body, levelPath) return updated } - // Set budget budgetPath := "request.generationConfig.thinkingConfig.thinkingBudget" updated, err := sjson.SetBytes(body, budgetPath, budget) if err != nil { return body } - // Remove level updated, err = sjson.DeleteBytes(updated, levelPath) if err != nil { return body diff --git a/internal/util/thinking.go b/internal/util/thinking.go index 77ec16ba..74808669 100644 --- a/internal/util/thinking.go +++ b/internal/util/thinking.go @@ -160,6 +160,34 @@ func ThinkingEffortToBudget(model, effort string) (int, bool) { } } +// ThinkingLevelToBudget maps a Gemini thinkingLevel to a numeric thinking budget (tokens). +// +// Mappings: +// - "minimal" -> 512 +// - "low" -> 1024 +// - "medium" -> 8192 +// - "high" -> 32768 +// +// Returns false when the level is empty or unsupported. +func ThinkingLevelToBudget(level string) (int, bool) { + if level == "" { + return 0, false + } + normalized := strings.ToLower(strings.TrimSpace(level)) + switch normalized { + case "minimal": + return 512, true + case "low": + return 1024, true + case "medium": + return 8192, true + case "high": + return 32768, true + default: + return 0, false + } +} + // ThinkingBudgetToEffort maps a numeric thinking budget (tokens) // to a reasoning effort level for level-based models. //