package util import ( "strings" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) const ( GeminiThinkingBudgetMetadataKey = "gemini_thinking_budget" GeminiIncludeThoughtsMetadataKey = "gemini_include_thoughts" GeminiOriginalModelMetadataKey = "gemini_original_model" ) func ApplyGeminiThinkingConfig(body []byte, budget *int, includeThoughts *bool) []byte { if budget == nil && includeThoughts == nil { return body } updated := body if budget != nil { valuePath := "generationConfig.thinkingConfig.thinkingBudget" rewritten, err := sjson.SetBytes(updated, valuePath, *budget) if err == nil { updated = rewritten } } // Default to including thoughts when a budget override is present but no explicit include flag is provided. incl := includeThoughts if incl == nil && budget != nil && *budget != 0 { defaultInclude := true incl = &defaultInclude } if incl != nil { valuePath := "generationConfig.thinkingConfig.include_thoughts" rewritten, err := sjson.SetBytes(updated, valuePath, *incl) if err == nil { updated = rewritten } } return updated } func ApplyGeminiCLIThinkingConfig(body []byte, budget *int, includeThoughts *bool) []byte { if budget == nil && includeThoughts == nil { return body } updated := body if budget != nil { valuePath := "request.generationConfig.thinkingConfig.thinkingBudget" rewritten, err := sjson.SetBytes(updated, valuePath, *budget) if err == nil { updated = rewritten } } // Default to including thoughts when a budget override is present but no explicit include flag is provided. incl := includeThoughts if incl == nil && budget != nil && *budget != 0 { defaultInclude := true incl = &defaultInclude } if incl != nil { valuePath := "request.generationConfig.thinkingConfig.include_thoughts" rewritten, err := sjson.SetBytes(updated, valuePath, *incl) if err == nil { updated = rewritten } } return updated } // modelsWithDefaultThinking lists models that should have thinking enabled by default // when no explicit thinkingConfig is provided. var modelsWithDefaultThinking = map[string]bool{ "gemini-3-pro-preview": true, } // ModelHasDefaultThinking returns true if the model should have thinking enabled by default. func ModelHasDefaultThinking(model string) bool { return modelsWithDefaultThinking[model] } // ApplyDefaultThinkingIfNeeded injects default thinkingConfig for models that require it. // For standard Gemini API format (generationConfig.thinkingConfig path). // Returns the modified body if thinkingConfig was added, otherwise returns the original. func ApplyDefaultThinkingIfNeeded(model string, body []byte) []byte { if !ModelHasDefaultThinking(model) { return body } if gjson.GetBytes(body, "generationConfig.thinkingConfig").Exists() { return body } updated, _ := sjson.SetBytes(body, "generationConfig.thinkingConfig.thinkingBudget", -1) updated, _ = sjson.SetBytes(updated, "generationConfig.thinkingConfig.include_thoughts", true) return updated } // ApplyDefaultThinkingIfNeededCLI injects default thinkingConfig for models that require it. // For Gemini CLI API format (request.generationConfig.thinkingConfig path). // Returns the modified body if thinkingConfig was added, otherwise returns the original. func ApplyDefaultThinkingIfNeededCLI(model string, body []byte) []byte { if !ModelHasDefaultThinking(model) { return body } if gjson.GetBytes(body, "request.generationConfig.thinkingConfig").Exists() { return body } updated, _ := sjson.SetBytes(body, "request.generationConfig.thinkingConfig.thinkingBudget", -1) updated, _ = sjson.SetBytes(updated, "request.generationConfig.thinkingConfig.include_thoughts", true) return updated } // StripThinkingConfigIfUnsupported removes thinkingConfig from the request body // when the target model does not advertise Thinking capability. It cleans both // standard Gemini and Gemini CLI JSON envelopes. This acts as a final safety net // in case upstream injected thinking for an unsupported model. func StripThinkingConfigIfUnsupported(model string, body []byte) []byte { if ModelSupportsThinking(model) || len(body) == 0 { return body } updated := body // Gemini CLI path updated, _ = sjson.DeleteBytes(updated, "request.generationConfig.thinkingConfig") // Standard Gemini path updated, _ = sjson.DeleteBytes(updated, "generationConfig.thinkingConfig") return updated } // NormalizeGeminiThinkingBudget normalizes the thinkingBudget value in a standard Gemini // request body (generationConfig.thinkingConfig.thinkingBudget path). func NormalizeGeminiThinkingBudget(model string, body []byte) []byte { const budgetPath = "generationConfig.thinkingConfig.thinkingBudget" budget := gjson.GetBytes(body, budgetPath) if !budget.Exists() { return body } normalized := NormalizeThinkingBudget(model, int(budget.Int())) updated, _ := sjson.SetBytes(body, budgetPath, normalized) return updated } // NormalizeGeminiCLIThinkingBudget normalizes the thinkingBudget value in a Gemini CLI // request body (request.generationConfig.thinkingConfig.thinkingBudget path). func NormalizeGeminiCLIThinkingBudget(model string, body []byte) []byte { const budgetPath = "request.generationConfig.thinkingConfig.thinkingBudget" budget := gjson.GetBytes(body, budgetPath) if !budget.Exists() { return body } normalized := NormalizeThinkingBudget(model, int(budget.Int())) updated, _ := sjson.SetBytes(body, budgetPath, normalized) return updated } // ReasoningEffortBudgetMapping defines the thinkingBudget values for each reasoning effort level. var ReasoningEffortBudgetMapping = map[string]int{ "none": 0, "auto": -1, "minimal": 512, "low": 1024, "medium": 8192, "high": 24576, "xhigh": 32768, } // ApplyReasoningEffortToGemini applies OpenAI reasoning_effort to Gemini thinkingConfig // for standard Gemini API format (generationConfig.thinkingConfig path). // Returns the modified body with thinkingBudget and include_thoughts set. func ApplyReasoningEffortToGemini(body []byte, effort string) []byte { normalized := strings.ToLower(strings.TrimSpace(effort)) if normalized == "" { return body } budgetPath := "generationConfig.thinkingConfig.thinkingBudget" includePath := "generationConfig.thinkingConfig.include_thoughts" if normalized == "none" { body, _ = sjson.DeleteBytes(body, "generationConfig.thinkingConfig") return body } budget, ok := ReasoningEffortBudgetMapping[normalized] if !ok { return body } body, _ = sjson.SetBytes(body, budgetPath, budget) body, _ = sjson.SetBytes(body, includePath, true) return body } // ApplyReasoningEffortToGeminiCLI applies OpenAI reasoning_effort to Gemini CLI thinkingConfig // for Gemini CLI API format (request.generationConfig.thinkingConfig path). // Returns the modified body with thinkingBudget and include_thoughts set. func ApplyReasoningEffortToGeminiCLI(body []byte, effort string) []byte { normalized := strings.ToLower(strings.TrimSpace(effort)) if normalized == "" { return body } budgetPath := "request.generationConfig.thinkingConfig.thinkingBudget" includePath := "request.generationConfig.thinkingConfig.include_thoughts" if normalized == "none" { body, _ = sjson.DeleteBytes(body, "request.generationConfig.thinkingConfig") return body } budget, ok := ReasoningEffortBudgetMapping[normalized] if !ok { return body } body, _ = sjson.SetBytes(body, budgetPath, budget) body, _ = sjson.SetBytes(body, includePath, true) return body } // ConvertThinkingLevelToBudget checks for "generationConfig.thinkingConfig.thinkingLevel" // and converts it to "thinkingBudget". // "high" -> 32768 // "low" -> 128 // It removes "thinkingLevel" after conversion. func ConvertThinkingLevelToBudget(body []byte) []byte { levelPath := "generationConfig.thinkingConfig.thinkingLevel" res := gjson.GetBytes(body, levelPath) if !res.Exists() { return body } level := strings.ToLower(res.String()) var budget int switch level { case "high": budget = 32768 case "low": budget = 128 default: // If unknown level, we might just leave it or default. // User only specified high and low. We'll assume we shouldn't touch it if it's something else, // or maybe we should just remove the invalid level? // For safety adhering to strict instructions: "If high... if low...". // If it's something else, the upstream might fail anyway if we leave it, // but let's just delete the level if we processed it. // Actually, let's check if we need to do anything for other values. // For now, only handle high/low. return body } // 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 } return updated }