| | package util |
| |
|
| | import ( |
| | "regexp" |
| | "strings" |
| |
|
| | "github.com/tidwall/gjson" |
| | "github.com/tidwall/sjson" |
| | ) |
| |
|
| | const ( |
| | GeminiThinkingBudgetMetadataKey = "gemini_thinking_budget" |
| | GeminiIncludeThoughtsMetadataKey = "gemini_include_thoughts" |
| | GeminiOriginalModelMetadataKey = "gemini_original_model" |
| | ) |
| |
|
| | |
| | var ( |
| | gemini3Pattern = regexp.MustCompile(`(?i)^gemini[_-]?3[_-]`) |
| | gemini3ProPattern = regexp.MustCompile(`(?i)^gemini[_-]?3[_-]pro`) |
| | gemini3FlashPattern = regexp.MustCompile(`(?i)^gemini[_-]?3[_-]flash`) |
| | gemini25Pattern = regexp.MustCompile(`(?i)^gemini[_-]?2\.5[_-]`) |
| | ) |
| |
|
| | |
| | |
| | func IsGemini3Model(model string) bool { |
| | return gemini3Pattern.MatchString(model) |
| | } |
| |
|
| | |
| | |
| | func IsGemini3ProModel(model string) bool { |
| | return gemini3ProPattern.MatchString(model) |
| | } |
| |
|
| | |
| | |
| | func IsGemini3FlashModel(model string) bool { |
| | return gemini3FlashPattern.MatchString(model) |
| | } |
| |
|
| | |
| | |
| | func IsGemini25Model(model string) bool { |
| | return gemini25Pattern.MatchString(model) |
| | } |
| |
|
| | |
| | var Gemini3ProThinkingLevels = []string{"low", "high"} |
| |
|
| | |
| | var Gemini3FlashThinkingLevels = []string{"minimal", "low", "medium", "high"} |
| |
|
| | 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 |
| | } |
| | } |
| | |
| | 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 |
| | } |
| | } |
| | |
| | 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 |
| | } |
| |
|
| | |
| | |
| | |
| | func ApplyGeminiThinkingLevel(body []byte, level string, includeThoughts *bool) []byte { |
| | if level == "" && includeThoughts == nil { |
| | return body |
| | } |
| | updated := body |
| | if level != "" { |
| | valuePath := "generationConfig.thinkingConfig.thinkingLevel" |
| | rewritten, err := sjson.SetBytes(updated, valuePath, level) |
| | if err == nil { |
| | updated = rewritten |
| | } |
| | } |
| | |
| | incl := includeThoughts |
| | if incl == nil && level != "" { |
| | defaultInclude := true |
| | incl = &defaultInclude |
| | } |
| | if incl != nil { |
| | valuePath := "generationConfig.thinkingConfig.includeThoughts" |
| | rewritten, err := sjson.SetBytes(updated, valuePath, *incl) |
| | if err == nil { |
| | updated = rewritten |
| | } |
| | } |
| | if it := gjson.GetBytes(body, "generationConfig.thinkingConfig.include_thoughts"); it.Exists() { |
| | updated, _ = sjson.DeleteBytes(updated, "generationConfig.thinkingConfig.include_thoughts") |
| | } |
| | if tb := gjson.GetBytes(body, "generationConfig.thinkingConfig.thinkingBudget"); tb.Exists() { |
| | updated, _ = sjson.DeleteBytes(updated, "generationConfig.thinkingConfig.thinkingBudget") |
| | } |
| | return updated |
| | } |
| |
|
| | |
| | |
| | |
| | func ApplyGeminiCLIThinkingLevel(body []byte, level string, includeThoughts *bool) []byte { |
| | if level == "" && includeThoughts == nil { |
| | return body |
| | } |
| | updated := body |
| | if level != "" { |
| | valuePath := "request.generationConfig.thinkingConfig.thinkingLevel" |
| | rewritten, err := sjson.SetBytes(updated, valuePath, level) |
| | if err == nil { |
| | updated = rewritten |
| | } |
| | } |
| | |
| | incl := includeThoughts |
| | if incl == nil && level != "" { |
| | defaultInclude := true |
| | incl = &defaultInclude |
| | } |
| | if incl != nil { |
| | valuePath := "request.generationConfig.thinkingConfig.includeThoughts" |
| | rewritten, err := sjson.SetBytes(updated, valuePath, *incl) |
| | if err == nil { |
| | updated = rewritten |
| | } |
| | } |
| | if it := gjson.GetBytes(body, "request.generationConfig.thinkingConfig.include_thoughts"); it.Exists() { |
| | updated, _ = sjson.DeleteBytes(updated, "request.generationConfig.thinkingConfig.include_thoughts") |
| | } |
| | if tb := gjson.GetBytes(body, "request.generationConfig.thinkingConfig.thinkingBudget"); tb.Exists() { |
| | updated, _ = sjson.DeleteBytes(updated, "request.generationConfig.thinkingConfig.thinkingBudget") |
| | } |
| | return updated |
| | } |
| |
|
| | |
| | |
| | func ValidateGemini3ThinkingLevel(model, level string) (string, bool) { |
| | if level == "" { |
| | return "", false |
| | } |
| | normalized := strings.ToLower(strings.TrimSpace(level)) |
| |
|
| | var validLevels []string |
| | if IsGemini3ProModel(model) { |
| | validLevels = Gemini3ProThinkingLevels |
| | } else if IsGemini3FlashModel(model) { |
| | validLevels = Gemini3FlashThinkingLevels |
| | } else if IsGemini3Model(model) { |
| | |
| | validLevels = Gemini3FlashThinkingLevels |
| | } else { |
| | return "", false |
| | } |
| |
|
| | for _, valid := range validLevels { |
| | if normalized == valid { |
| | return normalized, true |
| | } |
| | } |
| | return "", false |
| | } |
| |
|
| | |
| | |
| | |
| | func ThinkingBudgetToGemini3Level(model string, budget int) (string, bool) { |
| | if !IsGemini3Model(model) { |
| | return "", false |
| | } |
| |
|
| | |
| | |
| | |
| | switch { |
| | case budget == -1: |
| | |
| | return "high", true |
| | case budget == 0: |
| | |
| | |
| | if IsGemini3FlashModel(model) { |
| | return "minimal", true |
| | } |
| | return "low", true |
| | case budget > 0 && budget <= 512: |
| | if IsGemini3FlashModel(model) { |
| | return "minimal", true |
| | } |
| | return "low", true |
| | case budget <= 1024: |
| | return "low", true |
| | case budget <= 8192: |
| | if IsGemini3FlashModel(model) { |
| | return "medium", true |
| | } |
| | return "low", true |
| | default: |
| | return "high", true |
| | } |
| | } |
| |
|
| | |
| | |
| | var modelsWithDefaultThinking = map[string]bool{ |
| | "gemini-3-pro-preview": true, |
| | "gemini-3-pro-image-preview": true, |
| | |
| | } |
| |
|
| | |
| | func ModelHasDefaultThinking(model string) bool { |
| | return modelsWithDefaultThinking[model] |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | func ApplyDefaultThinkingIfNeeded(model string, body []byte) []byte { |
| | if !ModelHasDefaultThinking(model) { |
| | return body |
| | } |
| | if gjson.GetBytes(body, "generationConfig.thinkingConfig").Exists() { |
| | return body |
| | } |
| | |
| | if IsGemini3Model(model) { |
| | |
| | |
| | updated, _ := sjson.SetBytes(body, "generationConfig.thinkingConfig.includeThoughts", true) |
| | return updated |
| | } |
| | |
| | updated, _ := sjson.SetBytes(body, "generationConfig.thinkingConfig.thinkingBudget", -1) |
| | updated, _ = sjson.SetBytes(updated, "generationConfig.thinkingConfig.include_thoughts", true) |
| | return updated |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | func ApplyGemini3ThinkingLevelFromMetadata(model string, metadata map[string]any, body []byte) []byte { |
| | |
| | lookupModel := ResolveOriginalModel(model, metadata) |
| | if !IsGemini3Model(lookupModel) && !IsGemini3Model(model) { |
| | return body |
| | } |
| |
|
| | |
| | checkModel := model |
| | if IsGemini3Model(lookupModel) { |
| | checkModel = lookupModel |
| | } |
| |
|
| | |
| | effort, ok := ReasoningEffortFromMetadata(metadata) |
| | if ok && effort != "" { |
| | if level, valid := ValidateGemini3ThinkingLevel(checkModel, effort); valid { |
| | return ApplyGeminiThinkingLevel(body, level, nil) |
| | } |
| | } |
| |
|
| | |
| | budget, _, _, matched := ThinkingFromMetadata(metadata) |
| | if matched && budget != nil { |
| | if level, valid := ThinkingBudgetToGemini3Level(checkModel, *budget); valid { |
| | return ApplyGeminiThinkingLevel(body, level, nil) |
| | } |
| | } |
| |
|
| | return body |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | func ApplyGemini3ThinkingLevelFromMetadataCLI(model string, metadata map[string]any, body []byte) []byte { |
| | |
| | lookupModel := ResolveOriginalModel(model, metadata) |
| | if !IsGemini3Model(lookupModel) && !IsGemini3Model(model) { |
| | return body |
| | } |
| |
|
| | |
| | checkModel := model |
| | if IsGemini3Model(lookupModel) { |
| | checkModel = lookupModel |
| | } |
| |
|
| | |
| | effort, ok := ReasoningEffortFromMetadata(metadata) |
| | if ok && effort != "" { |
| | if level, valid := ValidateGemini3ThinkingLevel(checkModel, effort); valid { |
| | return ApplyGeminiCLIThinkingLevel(body, level, nil) |
| | } |
| | } |
| |
|
| | |
| | budget, _, _, matched := ThinkingFromMetadata(metadata) |
| | if matched && budget != nil { |
| | if level, valid := ThinkingBudgetToGemini3Level(checkModel, *budget); valid { |
| | return ApplyGeminiCLIThinkingLevel(body, level, nil) |
| | } |
| | } |
| |
|
| | return body |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | func ApplyDefaultThinkingIfNeededCLI(model string, metadata map[string]any, body []byte) []byte { |
| | |
| | lookupModel := ResolveOriginalModel(model, metadata) |
| | if !ModelHasDefaultThinking(lookupModel) && !ModelHasDefaultThinking(model) { |
| | return body |
| | } |
| | if gjson.GetBytes(body, "request.generationConfig.thinkingConfig").Exists() { |
| | return body |
| | } |
| | |
| | if IsGemini3Model(lookupModel) || IsGemini3Model(model) { |
| | |
| | |
| | updated, _ := sjson.SetBytes(body, "request.generationConfig.thinkingConfig.includeThoughts", true) |
| | return updated |
| | } |
| | |
| | updated, _ := sjson.SetBytes(body, "request.generationConfig.thinkingConfig.thinkingBudget", -1) |
| | updated, _ = sjson.SetBytes(updated, "request.generationConfig.thinkingConfig.include_thoughts", true) |
| | return updated |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | func StripThinkingConfigIfUnsupported(model string, body []byte) []byte { |
| | if ModelSupportsThinking(model) || len(body) == 0 { |
| | return body |
| | } |
| | updated := body |
| | |
| | updated, _ = sjson.DeleteBytes(updated, "request.generationConfig.thinkingConfig") |
| | |
| | updated, _ = sjson.DeleteBytes(updated, "generationConfig.thinkingConfig") |
| | return updated |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | func NormalizeGeminiThinkingBudget(model string, body []byte, skipGemini3Check ...bool) []byte { |
| | const budgetPath = "generationConfig.thinkingConfig.thinkingBudget" |
| | const levelPath = "generationConfig.thinkingConfig.thinkingLevel" |
| |
|
| | budget := gjson.GetBytes(body, budgetPath) |
| | if !budget.Exists() { |
| | return body |
| | } |
| |
|
| | |
| | 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) |
| | return updated |
| | } |
| | |
| | updated, _ := sjson.DeleteBytes(body, budgetPath) |
| | return updated |
| | } |
| |
|
| | |
| | normalized := NormalizeThinkingBudget(model, int(budget.Int())) |
| | updated, _ := sjson.SetBytes(body, budgetPath, normalized) |
| | return updated |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | func NormalizeGeminiCLIThinkingBudget(model string, body []byte, skipGemini3Check ...bool) []byte { |
| | const budgetPath = "request.generationConfig.thinkingConfig.thinkingBudget" |
| | const levelPath = "request.generationConfig.thinkingConfig.thinkingLevel" |
| |
|
| | budget := gjson.GetBytes(body, budgetPath) |
| | if !budget.Exists() { |
| | return body |
| | } |
| |
|
| | |
| | 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) |
| | return updated |
| | } |
| | |
| | updated, _ := sjson.DeleteBytes(body, budgetPath) |
| | return updated |
| | } |
| |
|
| | |
| | normalized := NormalizeThinkingBudget(model, int(budget.Int())) |
| | updated, _ := sjson.SetBytes(body, budgetPath, normalized) |
| | return updated |
| | } |
| |
|
| | |
| | var ReasoningEffortBudgetMapping = map[string]int{ |
| | "none": 0, |
| | "auto": -1, |
| | "minimal": 512, |
| | "low": 1024, |
| | "medium": 8192, |
| | "high": 24576, |
| | "xhigh": 32768, |
| | } |
| |
|
| | |
| | |
| | |
| | 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 |
| | } |
| |
|
| | |
| | |
| | |
| | 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 |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | func ConvertThinkingLevelToBudget(body []byte, model string, skipGemini3Check ...bool) []byte { |
| | levelPath := "generationConfig.thinkingConfig.thinkingLevel" |
| | res := gjson.GetBytes(body, levelPath) |
| | if !res.Exists() { |
| | return body |
| | } |
| |
|
| | |
| | skipGemini3 := len(skipGemini3Check) > 0 && skipGemini3Check[0] |
| | if IsGemini3Model(model) && !skipGemini3 { |
| | return body |
| | } |
| |
|
| | budget, ok := ThinkingLevelToBudget(res.String()) |
| | if !ok { |
| | updated, _ := sjson.DeleteBytes(body, levelPath) |
| | return updated |
| | } |
| |
|
| | budgetPath := "generationConfig.thinkingConfig.thinkingBudget" |
| | updated, err := sjson.SetBytes(body, budgetPath, budget) |
| | if err != nil { |
| | return body |
| | } |
| |
|
| | updated, err = sjson.DeleteBytes(updated, levelPath) |
| | if err != nil { |
| | return body |
| | } |
| | return updated |
| | } |
| |
|
| | |
| | |
| | |
| | func ConvertThinkingLevelToBudgetCLI(body []byte, model string) []byte { |
| | levelPath := "request.generationConfig.thinkingConfig.thinkingLevel" |
| | res := gjson.GetBytes(body, levelPath) |
| | if !res.Exists() { |
| | return body |
| | } |
| |
|
| | |
| | if IsGemini3Model(model) { |
| | return body |
| | } |
| |
|
| | budget, ok := ThinkingLevelToBudget(res.String()) |
| | if !ok { |
| | updated, _ := sjson.DeleteBytes(body, levelPath) |
| | return updated |
| | } |
| |
|
| | budgetPath := "request.generationConfig.thinkingConfig.thinkingBudget" |
| | updated, err := sjson.SetBytes(body, budgetPath, budget) |
| | if err != nil { |
| | return body |
| | } |
| |
|
| | updated, err = sjson.DeleteBytes(updated, levelPath) |
| | if err != nil { |
| | return body |
| | } |
| | return updated |
| | } |
| |
|