fix(gemini): add optional skip for gemini3 thinking conversion

This commit is contained in:
hkfires
2025-12-19 22:07:43 +08:00
parent 99478d13a8
commit 2039062845
3 changed files with 49 additions and 42 deletions

View File

@@ -325,8 +325,8 @@ func (e *AIStudioExecutor) translateRequest(req cliproxyexecutor.Request, opts c
payload = ApplyThinkingMetadata(payload, req.Metadata, req.Model) payload = ApplyThinkingMetadata(payload, req.Metadata, req.Model)
payload = util.ApplyGemini3ThinkingLevelFromMetadata(req.Model, req.Metadata, payload) payload = util.ApplyGemini3ThinkingLevelFromMetadata(req.Model, req.Metadata, payload)
payload = util.ApplyDefaultThinkingIfNeeded(req.Model, payload) payload = util.ApplyDefaultThinkingIfNeeded(req.Model, payload)
payload = util.ConvertThinkingLevelToBudget(payload, req.Model) payload = util.ConvertThinkingLevelToBudget(payload, req.Model, true)
payload = util.NormalizeGeminiThinkingBudget(req.Model, payload) payload = util.NormalizeGeminiThinkingBudget(req.Model, payload, true)
payload = util.StripThinkingConfigIfUnsupported(req.Model, payload) payload = util.StripThinkingConfigIfUnsupported(req.Model, payload)
payload = fixGeminiImageAspectRatio(req.Model, payload) payload = fixGeminiImageAspectRatio(req.Model, payload)
payload = applyPayloadConfig(e.cfg, req.Model, payload) payload = applyPayloadConfig(e.cfg, req.Model, payload)

View File

@@ -352,8 +352,9 @@ func StripThinkingConfigIfUnsupported(model string, body []byte) []byte {
// NormalizeGeminiThinkingBudget normalizes the thinkingBudget value in a standard Gemini // NormalizeGeminiThinkingBudget normalizes the thinkingBudget value in a standard Gemini
// request body (generationConfig.thinkingConfig.thinkingBudget path). // request body (generationConfig.thinkingConfig.thinkingBudget path).
// For Gemini 3 models, converts thinkingBudget to thinkingLevel per Google's documentation. // For Gemini 3 models, converts thinkingBudget to thinkingLevel per Google's documentation,
func NormalizeGeminiThinkingBudget(model string, body []byte) []byte { // unless skipGemini3Check is provided and true.
func NormalizeGeminiThinkingBudget(model string, body []byte, skipGemini3Check ...bool) []byte {
const budgetPath = "generationConfig.thinkingConfig.thinkingBudget" const budgetPath = "generationConfig.thinkingConfig.thinkingBudget"
const levelPath = "generationConfig.thinkingConfig.thinkingLevel" const levelPath = "generationConfig.thinkingConfig.thinkingLevel"
@@ -363,7 +364,8 @@ func NormalizeGeminiThinkingBudget(model string, body []byte) []byte {
} }
// For Gemini 3 models, convert thinkingBudget to thinkingLevel // 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 { if level, ok := ThinkingBudgetToGemini3Level(model, int(budget.Int())); ok {
updated, _ := sjson.SetBytes(body, levelPath, level) updated, _ := sjson.SetBytes(body, levelPath, level)
updated, _ = sjson.DeleteBytes(updated, budgetPath) 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 // NormalizeGeminiCLIThinkingBudget normalizes the thinkingBudget value in a Gemini CLI
// request body (request.generationConfig.thinkingConfig.thinkingBudget path). // request body (request.generationConfig.thinkingConfig.thinkingBudget path).
// For Gemini 3 models, converts thinkingBudget to thinkingLevel per Google's documentation. // For Gemini 3 models, converts thinkingBudget to thinkingLevel per Google's documentation,
func NormalizeGeminiCLIThinkingBudget(model string, body []byte) []byte { // unless skipGemini3Check is provided and true.
func NormalizeGeminiCLIThinkingBudget(model string, body []byte, skipGemini3Check ...bool) []byte {
const budgetPath = "request.generationConfig.thinkingConfig.thinkingBudget" const budgetPath = "request.generationConfig.thinkingConfig.thinkingBudget"
const levelPath = "request.generationConfig.thinkingConfig.thinkingLevel" 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 // 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 { if level, ok := ThinkingBudgetToGemini3Level(model, int(budget.Int())); ok {
updated, _ := sjson.SetBytes(body, levelPath, level) updated, _ := sjson.SetBytes(body, levelPath, level)
updated, _ = sjson.DeleteBytes(updated, budgetPath) updated, _ = sjson.DeleteBytes(updated, budgetPath)
@@ -477,7 +481,7 @@ func ApplyReasoningEffortToGeminiCLI(body []byte, effort string) []byte {
// ConvertThinkingLevelToBudget checks for "generationConfig.thinkingConfig.thinkingLevel" // ConvertThinkingLevelToBudget checks for "generationConfig.thinkingConfig.thinkingLevel"
// and converts it to "thinkingBudget" for Gemini 2.5 models. // 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: // Mappings for Gemini 2.5:
// - "high" -> 32768 // - "high" -> 32768
// - "medium" -> 8192 // - "medium" -> 8192
@@ -485,43 +489,31 @@ func ApplyReasoningEffortToGeminiCLI(body []byte, effort string) []byte {
// - "minimal" -> 512 // - "minimal" -> 512
// //
// It removes "thinkingLevel" after conversion (for Gemini 2.5 only). // 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" levelPath := "generationConfig.thinkingConfig.thinkingLevel"
res := gjson.GetBytes(body, levelPath) res := gjson.GetBytes(body, levelPath)
if !res.Exists() { if !res.Exists() {
return body return body
} }
// For Gemini 3 models, preserve thinkingLevel - don't convert to budget // For Gemini 3 models, preserve thinkingLevel unless explicitly skipped
if IsGemini3Model(model) { skipGemini3 := len(skipGemini3Check) > 0 && skipGemini3Check[0]
if IsGemini3Model(model) && !skipGemini3 {
return body return body
} }
level := strings.ToLower(res.String()) budget, ok := ThinkingLevelToBudget(res.String())
var budget int if !ok {
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
updated, _ := sjson.DeleteBytes(body, levelPath) updated, _ := sjson.DeleteBytes(body, levelPath)
return updated return updated
} }
// Set budget
budgetPath := "generationConfig.thinkingConfig.thinkingBudget" budgetPath := "generationConfig.thinkingConfig.thinkingBudget"
updated, err := sjson.SetBytes(body, budgetPath, budget) updated, err := sjson.SetBytes(body, budgetPath, budget)
if err != nil { if err != nil {
return body return body
} }
// Remove level
updated, err = sjson.DeleteBytes(updated, levelPath) updated, err = sjson.DeleteBytes(updated, levelPath)
if err != nil { if err != nil {
return body return body
@@ -544,31 +536,18 @@ func ConvertThinkingLevelToBudgetCLI(body []byte, model string) []byte {
return body return body
} }
level := strings.ToLower(res.String()) budget, ok := ThinkingLevelToBudget(res.String())
var budget int if !ok {
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
updated, _ := sjson.DeleteBytes(body, levelPath) updated, _ := sjson.DeleteBytes(body, levelPath)
return updated return updated
} }
// Set budget
budgetPath := "request.generationConfig.thinkingConfig.thinkingBudget" budgetPath := "request.generationConfig.thinkingConfig.thinkingBudget"
updated, err := sjson.SetBytes(body, budgetPath, budget) updated, err := sjson.SetBytes(body, budgetPath, budget)
if err != nil { if err != nil {
return body return body
} }
// Remove level
updated, err = sjson.DeleteBytes(updated, levelPath) updated, err = sjson.DeleteBytes(updated, levelPath)
if err != nil { if err != nil {
return body return body

View File

@@ -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) // ThinkingBudgetToEffort maps a numeric thinking budget (tokens)
// to a reasoning effort level for level-based models. // to a reasoning effort level for level-based models.
// //