mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-03-09 15:25:17 +00:00
Move OpenAI `reasoning_effort` -> Gemini `thinkingConfig` budget logic into shared helpers used by Gemini, Gemini CLI, and antigravity translators. Normalize Claude thinking handling by preferring positive budgets, applying budget token normalization, and gating by model support. Always convert Gemini `thinkingBudget` back to OpenAI `reasoning_effort` to support allowCompat models, and update tests for normalization behavior.
253 lines
8.5 KiB
Go
253 lines
8.5 KiB
Go
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 {
|
|
budget, ok := ReasoningEffortBudgetMapping[effort]
|
|
if !ok {
|
|
budget = -1 // default to auto
|
|
}
|
|
|
|
budgetPath := "generationConfig.thinkingConfig.thinkingBudget"
|
|
includePath := "generationConfig.thinkingConfig.include_thoughts"
|
|
|
|
if effort == "none" {
|
|
body, _ = sjson.DeleteBytes(body, "generationConfig.thinkingConfig")
|
|
} else {
|
|
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 {
|
|
budget, ok := ReasoningEffortBudgetMapping[effort]
|
|
if !ok {
|
|
budget = -1 // default to auto
|
|
}
|
|
|
|
budgetPath := "request.generationConfig.thinkingConfig.thinkingBudget"
|
|
includePath := "request.generationConfig.thinkingConfig.include_thoughts"
|
|
|
|
if effort == "none" {
|
|
body, _ = sjson.DeleteBytes(body, "request.generationConfig.thinkingConfig")
|
|
} else {
|
|
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
|
|
}
|