mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 04:50:52 +08:00
feat: use thinkingLevel for Gemini 3 models per Google documentation
Per Google's official documentation, Gemini 3 models should use thinkingLevel (string) instead of thinkingBudget (number) for optimal performance. From Google's Gemini Thinking docs: > Use the thinkingLevel parameter with Gemini 3 models. While > thinkingBudget is accepted for backwards compatibility, using > it with Gemini 3 Pro may result in suboptimal performance. Changes: - Add model family detection functions (IsGemini3Model, IsGemini25Model, IsGemini3ProModel, IsGemini3FlashModel) - Add ApplyGeminiThinkingLevel and ApplyGeminiCLIThinkingLevel functions for applying thinkingLevel config - Add ValidateGemini3ThinkingLevel for model-specific level validation - Add ThinkingBudgetToGemini3Level for backward compatibility conversion - Update NormalizeGeminiThinkingBudget to convert budget to level for Gemini 3 models - Update ApplyDefaultThinkingIfNeeded to not set a default level for Gemini 3 (lets API use its dynamic default "high") - Update ConvertThinkingLevelToBudget to preserve thinkingLevel for Gemini 3 models - Add Levels field to all Gemini 3 model definitions: - Gemini 3 Pro: ["low", "high"] - Gemini 3 Flash: ["minimal", "low", "medium", "high"] Backward compatibility: - Gemini 2.5 models continue to use thinkingBudget as before - If thinkingBudget is provided for Gemini 3, it's converted to the appropriate thinkingLevel - Existing configurations continue to work
This commit is contained in:
@@ -160,7 +160,7 @@ func GetGeminiModels() []*ModelInfo {
|
|||||||
InputTokenLimit: 1048576,
|
InputTokenLimit: 1048576,
|
||||||
OutputTokenLimit: 65536,
|
OutputTokenLimit: 65536,
|
||||||
SupportedGenerationMethods: []string{"generateContent", "countTokens", "createCachedContent", "batchGenerateContent"},
|
SupportedGenerationMethods: []string{"generateContent", "countTokens", "createCachedContent", "batchGenerateContent"},
|
||||||
Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true},
|
Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true, Levels: []string{"low", "high"}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "gemini-3-pro-image-preview",
|
ID: "gemini-3-pro-image-preview",
|
||||||
@@ -175,7 +175,7 @@ func GetGeminiModels() []*ModelInfo {
|
|||||||
InputTokenLimit: 1048576,
|
InputTokenLimit: 1048576,
|
||||||
OutputTokenLimit: 65536,
|
OutputTokenLimit: 65536,
|
||||||
SupportedGenerationMethods: []string{"generateContent", "countTokens", "createCachedContent", "batchGenerateContent"},
|
SupportedGenerationMethods: []string{"generateContent", "countTokens", "createCachedContent", "batchGenerateContent"},
|
||||||
Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true},
|
Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true, Levels: []string{"low", "high"}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,7 +240,7 @@ func GetGeminiVertexModels() []*ModelInfo {
|
|||||||
InputTokenLimit: 1048576,
|
InputTokenLimit: 1048576,
|
||||||
OutputTokenLimit: 65536,
|
OutputTokenLimit: 65536,
|
||||||
SupportedGenerationMethods: []string{"generateContent", "countTokens", "createCachedContent", "batchGenerateContent"},
|
SupportedGenerationMethods: []string{"generateContent", "countTokens", "createCachedContent", "batchGenerateContent"},
|
||||||
Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true},
|
Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true, Levels: []string{"low", "high"}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "gemini-3-flash-preview",
|
ID: "gemini-3-flash-preview",
|
||||||
@@ -255,7 +255,7 @@ func GetGeminiVertexModels() []*ModelInfo {
|
|||||||
InputTokenLimit: 1048576,
|
InputTokenLimit: 1048576,
|
||||||
OutputTokenLimit: 65536,
|
OutputTokenLimit: 65536,
|
||||||
SupportedGenerationMethods: []string{"generateContent", "countTokens", "createCachedContent", "batchGenerateContent"},
|
SupportedGenerationMethods: []string{"generateContent", "countTokens", "createCachedContent", "batchGenerateContent"},
|
||||||
Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true},
|
Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true, Levels: []string{"minimal", "low", "medium", "high"}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "gemini-3-pro-image-preview",
|
ID: "gemini-3-pro-image-preview",
|
||||||
@@ -270,7 +270,7 @@ func GetGeminiVertexModels() []*ModelInfo {
|
|||||||
InputTokenLimit: 1048576,
|
InputTokenLimit: 1048576,
|
||||||
OutputTokenLimit: 65536,
|
OutputTokenLimit: 65536,
|
||||||
SupportedGenerationMethods: []string{"generateContent", "countTokens", "createCachedContent", "batchGenerateContent"},
|
SupportedGenerationMethods: []string{"generateContent", "countTokens", "createCachedContent", "batchGenerateContent"},
|
||||||
Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true},
|
Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true, Levels: []string{"low", "high"}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -336,7 +336,7 @@ func GetGeminiCLIModels() []*ModelInfo {
|
|||||||
InputTokenLimit: 1048576,
|
InputTokenLimit: 1048576,
|
||||||
OutputTokenLimit: 65536,
|
OutputTokenLimit: 65536,
|
||||||
SupportedGenerationMethods: []string{"generateContent", "countTokens", "createCachedContent", "batchGenerateContent"},
|
SupportedGenerationMethods: []string{"generateContent", "countTokens", "createCachedContent", "batchGenerateContent"},
|
||||||
Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true},
|
Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true, Levels: []string{"low", "high"}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "gemini-3-flash-preview",
|
ID: "gemini-3-flash-preview",
|
||||||
@@ -351,7 +351,7 @@ func GetGeminiCLIModels() []*ModelInfo {
|
|||||||
InputTokenLimit: 1048576,
|
InputTokenLimit: 1048576,
|
||||||
OutputTokenLimit: 65536,
|
OutputTokenLimit: 65536,
|
||||||
SupportedGenerationMethods: []string{"generateContent", "countTokens", "createCachedContent", "batchGenerateContent"},
|
SupportedGenerationMethods: []string{"generateContent", "countTokens", "createCachedContent", "batchGenerateContent"},
|
||||||
Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true},
|
Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true, Levels: []string{"minimal", "low", "medium", "high"}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -417,7 +417,7 @@ func GetAIStudioModels() []*ModelInfo {
|
|||||||
InputTokenLimit: 1048576,
|
InputTokenLimit: 1048576,
|
||||||
OutputTokenLimit: 65536,
|
OutputTokenLimit: 65536,
|
||||||
SupportedGenerationMethods: []string{"generateContent", "countTokens", "createCachedContent", "batchGenerateContent"},
|
SupportedGenerationMethods: []string{"generateContent", "countTokens", "createCachedContent", "batchGenerateContent"},
|
||||||
Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true},
|
Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true, Levels: []string{"low", "high"}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "gemini-3-flash-preview",
|
ID: "gemini-3-flash-preview",
|
||||||
@@ -432,7 +432,7 @@ func GetAIStudioModels() []*ModelInfo {
|
|||||||
InputTokenLimit: 1048576,
|
InputTokenLimit: 1048576,
|
||||||
OutputTokenLimit: 65536,
|
OutputTokenLimit: 65536,
|
||||||
SupportedGenerationMethods: []string{"generateContent", "countTokens", "createCachedContent", "batchGenerateContent"},
|
SupportedGenerationMethods: []string{"generateContent", "countTokens", "createCachedContent", "batchGenerateContent"},
|
||||||
Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true},
|
Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true, Levels: []string{"minimal", "low", "medium", "high"}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "gemini-pro-latest",
|
ID: "gemini-pro-latest",
|
||||||
@@ -743,8 +743,9 @@ func GetAntigravityModelConfig() map[string]*AntigravityModelConfig {
|
|||||||
"gemini-2.5-flash": {Thinking: &ThinkingSupport{Min: 0, Max: 24576, ZeroAllowed: true, DynamicAllowed: true}, Name: "models/gemini-2.5-flash"},
|
"gemini-2.5-flash": {Thinking: &ThinkingSupport{Min: 0, Max: 24576, ZeroAllowed: true, DynamicAllowed: true}, Name: "models/gemini-2.5-flash"},
|
||||||
"gemini-2.5-flash-lite": {Thinking: &ThinkingSupport{Min: 0, Max: 24576, ZeroAllowed: true, DynamicAllowed: true}, Name: "models/gemini-2.5-flash-lite"},
|
"gemini-2.5-flash-lite": {Thinking: &ThinkingSupport{Min: 0, Max: 24576, ZeroAllowed: true, DynamicAllowed: true}, Name: "models/gemini-2.5-flash-lite"},
|
||||||
"gemini-2.5-computer-use-preview-10-2025": {Name: "models/gemini-2.5-computer-use-preview-10-2025"},
|
"gemini-2.5-computer-use-preview-10-2025": {Name: "models/gemini-2.5-computer-use-preview-10-2025"},
|
||||||
"gemini-3-pro-preview": {Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true}, Name: "models/gemini-3-pro-preview"},
|
"gemini-3-pro-preview": {Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true, Levels: []string{"low", "high"}}, Name: "models/gemini-3-pro-preview"},
|
||||||
"gemini-3-pro-image-preview": {Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true}, Name: "models/gemini-3-pro-image-preview"},
|
"gemini-3-pro-image-preview": {Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true, Levels: []string{"low", "high"}}, Name: "models/gemini-3-pro-image-preview"},
|
||||||
|
"gemini-3-flash-preview": {Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true, Levels: []string{"minimal", "low", "medium", "high"}}, Name: "models/gemini-3-flash-preview"},
|
||||||
"gemini-claude-sonnet-4-5-thinking": {Thinking: &ThinkingSupport{Min: 1024, Max: 200000, ZeroAllowed: false, DynamicAllowed: true}, MaxCompletionTokens: 64000},
|
"gemini-claude-sonnet-4-5-thinking": {Thinking: &ThinkingSupport{Min: 1024, Max: 200000, ZeroAllowed: false, DynamicAllowed: true}, MaxCompletionTokens: 64000},
|
||||||
"gemini-claude-opus-4-5-thinking": {Thinking: &ThinkingSupport{Min: 1024, Max: 200000, ZeroAllowed: false, DynamicAllowed: true}, MaxCompletionTokens: 64000},
|
"gemini-claude-opus-4-5-thinking": {Thinking: &ThinkingSupport{Min: 1024, Max: 200000, ZeroAllowed: false, DynamicAllowed: true}, MaxCompletionTokens: 64000},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ func (e *AIStudioExecutor) translateRequest(req cliproxyexecutor.Request, opts c
|
|||||||
payload := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), stream)
|
payload := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), stream)
|
||||||
payload = ApplyThinkingMetadata(payload, req.Metadata, req.Model)
|
payload = ApplyThinkingMetadata(payload, req.Metadata, req.Model)
|
||||||
payload = util.ApplyDefaultThinkingIfNeeded(req.Model, payload)
|
payload = util.ApplyDefaultThinkingIfNeeded(req.Model, payload)
|
||||||
payload = util.ConvertThinkingLevelToBudget(payload)
|
payload = util.ConvertThinkingLevelToBudget(payload, req.Model)
|
||||||
payload = util.NormalizeGeminiThinkingBudget(req.Model, payload)
|
payload = util.NormalizeGeminiThinkingBudget(req.Model, payload)
|
||||||
payload = util.StripThinkingConfigIfUnsupported(req.Model, payload)
|
payload = util.StripThinkingConfigIfUnsupported(req.Model, payload)
|
||||||
payload = fixGeminiImageAspectRatio(req.Model, payload)
|
payload = fixGeminiImageAspectRatio(req.Model, payload)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
@@ -13,6 +14,44 @@ const (
|
|||||||
GeminiOriginalModelMetadataKey = "gemini_original_model"
|
GeminiOriginalModelMetadataKey = "gemini_original_model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Gemini model family detection patterns
|
||||||
|
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[_-]`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsGemini3Model returns true if the model is a Gemini 3 family model.
|
||||||
|
// Gemini 3 models should use thinkingLevel (string) instead of thinkingBudget (number).
|
||||||
|
func IsGemini3Model(model string) bool {
|
||||||
|
return gemini3Pattern.MatchString(model)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsGemini3ProModel returns true if the model is a Gemini 3 Pro variant.
|
||||||
|
// Gemini 3 Pro supports thinkingLevel: "low", "high" (default: "high")
|
||||||
|
func IsGemini3ProModel(model string) bool {
|
||||||
|
return gemini3ProPattern.MatchString(model)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsGemini3FlashModel returns true if the model is a Gemini 3 Flash variant.
|
||||||
|
// Gemini 3 Flash supports thinkingLevel: "minimal", "low", "medium", "high" (default: "high")
|
||||||
|
func IsGemini3FlashModel(model string) bool {
|
||||||
|
return gemini3FlashPattern.MatchString(model)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsGemini25Model returns true if the model is a Gemini 2.5 family model.
|
||||||
|
// Gemini 2.5 models should use thinkingBudget (number).
|
||||||
|
func IsGemini25Model(model string) bool {
|
||||||
|
return gemini25Pattern.MatchString(model)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gemini3ProThinkingLevels are the valid thinkingLevel values for Gemini 3 Pro models.
|
||||||
|
var Gemini3ProThinkingLevels = []string{"low", "high"}
|
||||||
|
|
||||||
|
// Gemini3FlashThinkingLevels are the valid thinkingLevel values for Gemini 3 Flash models.
|
||||||
|
var Gemini3FlashThinkingLevels = []string{"minimal", "low", "medium", "high"}
|
||||||
|
|
||||||
func ApplyGeminiThinkingConfig(body []byte, budget *int, includeThoughts *bool) []byte {
|
func ApplyGeminiThinkingConfig(body []byte, budget *int, includeThoughts *bool) []byte {
|
||||||
if budget == nil && includeThoughts == nil {
|
if budget == nil && includeThoughts == nil {
|
||||||
return body
|
return body
|
||||||
@@ -69,10 +108,141 @@ func ApplyGeminiCLIThinkingConfig(body []byte, budget *int, includeThoughts *boo
|
|||||||
return updated
|
return updated
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ApplyGeminiThinkingLevel applies thinkingLevel config for Gemini 3 models.
|
||||||
|
// For standard Gemini API format (generationConfig.thinkingConfig path).
|
||||||
|
// Per Google's documentation, Gemini 3 models should use thinkingLevel instead of thinkingBudget.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Default to including thoughts when a level is set but no explicit include flag is provided.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return updated
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyGeminiCLIThinkingLevel applies thinkingLevel config for Gemini 3 models.
|
||||||
|
// For Gemini CLI API format (request.generationConfig.thinkingConfig path).
|
||||||
|
// Per Google's documentation, Gemini 3 models should use thinkingLevel instead of thinkingBudget.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Default to including thoughts when a level is set but no explicit include flag is provided.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return updated
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateGemini3ThinkingLevel validates that the thinkingLevel is valid for the Gemini 3 model variant.
|
||||||
|
// Returns the validated level (normalized to lowercase) and true if valid, or empty string and false if invalid.
|
||||||
|
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) {
|
||||||
|
// Unknown Gemini 3 variant - allow all levels as fallback
|
||||||
|
validLevels = Gemini3FlashThinkingLevels
|
||||||
|
} else {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, valid := range validLevels {
|
||||||
|
if normalized == valid {
|
||||||
|
return normalized, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ThinkingBudgetToGemini3Level converts a thinkingBudget to a thinkingLevel for Gemini 3 models.
|
||||||
|
// This provides backward compatibility when thinkingBudget is provided for Gemini 3 models.
|
||||||
|
// Returns the appropriate thinkingLevel and true if conversion is possible.
|
||||||
|
func ThinkingBudgetToGemini3Level(model string, budget int) (string, bool) {
|
||||||
|
if !IsGemini3Model(model) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map budget to level based on Google's documentation
|
||||||
|
// Gemini 3 Pro: "low", "high" (default: "high")
|
||||||
|
// Gemini 3 Flash: "minimal", "low", "medium", "high" (default: "high")
|
||||||
|
switch {
|
||||||
|
case budget == -1:
|
||||||
|
// Dynamic budget maps to "high" (API default)
|
||||||
|
return "high", true
|
||||||
|
case budget == 0:
|
||||||
|
// Zero budget - Gemini 3 doesn't support disabling thinking
|
||||||
|
// Map to lowest available level
|
||||||
|
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 // Pro doesn't have medium, use low
|
||||||
|
default:
|
||||||
|
return "high", true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// modelsWithDefaultThinking lists models that should have thinking enabled by default
|
// modelsWithDefaultThinking lists models that should have thinking enabled by default
|
||||||
// when no explicit thinkingConfig is provided.
|
// when no explicit thinkingConfig is provided.
|
||||||
var modelsWithDefaultThinking = map[string]bool{
|
var modelsWithDefaultThinking = map[string]bool{
|
||||||
"gemini-3-pro-preview": true,
|
"gemini-3-pro-preview": true,
|
||||||
|
"gemini-3-pro-image-preview": true,
|
||||||
|
"gemini-3-flash-preview": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ModelHasDefaultThinking returns true if the model should have thinking enabled by default.
|
// ModelHasDefaultThinking returns true if the model should have thinking enabled by default.
|
||||||
@@ -83,6 +253,7 @@ func ModelHasDefaultThinking(model string) bool {
|
|||||||
// ApplyDefaultThinkingIfNeeded injects default thinkingConfig for models that require it.
|
// ApplyDefaultThinkingIfNeeded injects default thinkingConfig for models that require it.
|
||||||
// For standard Gemini API format (generationConfig.thinkingConfig path).
|
// For standard Gemini API format (generationConfig.thinkingConfig path).
|
||||||
// Returns the modified body if thinkingConfig was added, otherwise returns the original.
|
// Returns the modified body if thinkingConfig was added, otherwise returns the original.
|
||||||
|
// For Gemini 3 models, uses thinkingLevel instead of thinkingBudget per Google's documentation.
|
||||||
func ApplyDefaultThinkingIfNeeded(model string, body []byte) []byte {
|
func ApplyDefaultThinkingIfNeeded(model string, body []byte) []byte {
|
||||||
if !ModelHasDefaultThinking(model) {
|
if !ModelHasDefaultThinking(model) {
|
||||||
return body
|
return body
|
||||||
@@ -90,6 +261,14 @@ func ApplyDefaultThinkingIfNeeded(model string, body []byte) []byte {
|
|||||||
if gjson.GetBytes(body, "generationConfig.thinkingConfig").Exists() {
|
if gjson.GetBytes(body, "generationConfig.thinkingConfig").Exists() {
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
// Gemini 3 models use thinkingLevel instead of thinkingBudget
|
||||||
|
if IsGemini3Model(model) {
|
||||||
|
// Don't set a default - let the API use its dynamic default ("high")
|
||||||
|
// Only set includeThoughts
|
||||||
|
updated, _ := sjson.SetBytes(body, "generationConfig.thinkingConfig.includeThoughts", true)
|
||||||
|
return updated
|
||||||
|
}
|
||||||
|
// Gemini 2.5 and other models use thinkingBudget
|
||||||
updated, _ := sjson.SetBytes(body, "generationConfig.thinkingConfig.thinkingBudget", -1)
|
updated, _ := sjson.SetBytes(body, "generationConfig.thinkingConfig.thinkingBudget", -1)
|
||||||
updated, _ = sjson.SetBytes(updated, "generationConfig.thinkingConfig.include_thoughts", true)
|
updated, _ = sjson.SetBytes(updated, "generationConfig.thinkingConfig.include_thoughts", true)
|
||||||
return updated
|
return updated
|
||||||
@@ -98,6 +277,7 @@ func ApplyDefaultThinkingIfNeeded(model string, body []byte) []byte {
|
|||||||
// ApplyDefaultThinkingIfNeededCLI injects default thinkingConfig for models that require it.
|
// ApplyDefaultThinkingIfNeededCLI injects default thinkingConfig for models that require it.
|
||||||
// For Gemini CLI API format (request.generationConfig.thinkingConfig path).
|
// For Gemini CLI API format (request.generationConfig.thinkingConfig path).
|
||||||
// Returns the modified body if thinkingConfig was added, otherwise returns the original.
|
// Returns the modified body if thinkingConfig was added, otherwise returns the original.
|
||||||
|
// For Gemini 3 models, uses thinkingLevel instead of thinkingBudget per Google's documentation.
|
||||||
func ApplyDefaultThinkingIfNeededCLI(model string, body []byte) []byte {
|
func ApplyDefaultThinkingIfNeededCLI(model string, body []byte) []byte {
|
||||||
if !ModelHasDefaultThinking(model) {
|
if !ModelHasDefaultThinking(model) {
|
||||||
return body
|
return body
|
||||||
@@ -105,6 +285,14 @@ func ApplyDefaultThinkingIfNeededCLI(model string, body []byte) []byte {
|
|||||||
if gjson.GetBytes(body, "request.generationConfig.thinkingConfig").Exists() {
|
if gjson.GetBytes(body, "request.generationConfig.thinkingConfig").Exists() {
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
// Gemini 3 models use thinkingLevel instead of thinkingBudget
|
||||||
|
if IsGemini3Model(model) {
|
||||||
|
// Don't set a default - let the API use its dynamic default ("high")
|
||||||
|
// Only set includeThoughts
|
||||||
|
updated, _ := sjson.SetBytes(body, "request.generationConfig.thinkingConfig.includeThoughts", true)
|
||||||
|
return updated
|
||||||
|
}
|
||||||
|
// Gemini 2.5 and other models use thinkingBudget
|
||||||
updated, _ := sjson.SetBytes(body, "request.generationConfig.thinkingConfig.thinkingBudget", -1)
|
updated, _ := sjson.SetBytes(body, "request.generationConfig.thinkingConfig.thinkingBudget", -1)
|
||||||
updated, _ = sjson.SetBytes(updated, "request.generationConfig.thinkingConfig.include_thoughts", true)
|
updated, _ = sjson.SetBytes(updated, "request.generationConfig.thinkingConfig.include_thoughts", true)
|
||||||
return updated
|
return updated
|
||||||
@@ -128,12 +316,29 @@ 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.
|
||||||
func NormalizeGeminiThinkingBudget(model string, body []byte) []byte {
|
func NormalizeGeminiThinkingBudget(model string, body []byte) []byte {
|
||||||
const budgetPath = "generationConfig.thinkingConfig.thinkingBudget"
|
const budgetPath = "generationConfig.thinkingConfig.thinkingBudget"
|
||||||
|
const levelPath = "generationConfig.thinkingConfig.thinkingLevel"
|
||||||
|
|
||||||
budget := gjson.GetBytes(body, budgetPath)
|
budget := gjson.GetBytes(body, budgetPath)
|
||||||
if !budget.Exists() {
|
if !budget.Exists() {
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For Gemini 3 models, convert thinkingBudget to thinkingLevel
|
||||||
|
if IsGemini3Model(model) {
|
||||||
|
if level, ok := ThinkingBudgetToGemini3Level(model, int(budget.Int())); ok {
|
||||||
|
updated, _ := sjson.SetBytes(body, levelPath, level)
|
||||||
|
updated, _ = sjson.DeleteBytes(updated, budgetPath)
|
||||||
|
return updated
|
||||||
|
}
|
||||||
|
// If conversion fails, just remove the budget (let API use default)
|
||||||
|
updated, _ := sjson.DeleteBytes(body, budgetPath)
|
||||||
|
return updated
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Gemini 2.5 and other models, normalize the budget value
|
||||||
normalized := NormalizeThinkingBudget(model, int(budget.Int()))
|
normalized := NormalizeThinkingBudget(model, int(budget.Int()))
|
||||||
updated, _ := sjson.SetBytes(body, budgetPath, normalized)
|
updated, _ := sjson.SetBytes(body, budgetPath, normalized)
|
||||||
return updated
|
return updated
|
||||||
@@ -141,12 +346,29 @@ 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.
|
||||||
func NormalizeGeminiCLIThinkingBudget(model string, body []byte) []byte {
|
func NormalizeGeminiCLIThinkingBudget(model string, body []byte) []byte {
|
||||||
const budgetPath = "request.generationConfig.thinkingConfig.thinkingBudget"
|
const budgetPath = "request.generationConfig.thinkingConfig.thinkingBudget"
|
||||||
|
const levelPath = "request.generationConfig.thinkingConfig.thinkingLevel"
|
||||||
|
|
||||||
budget := gjson.GetBytes(body, budgetPath)
|
budget := gjson.GetBytes(body, budgetPath)
|
||||||
if !budget.Exists() {
|
if !budget.Exists() {
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For Gemini 3 models, convert thinkingBudget to thinkingLevel
|
||||||
|
if IsGemini3Model(model) {
|
||||||
|
if level, ok := ThinkingBudgetToGemini3Level(model, int(budget.Int())); ok {
|
||||||
|
updated, _ := sjson.SetBytes(body, levelPath, level)
|
||||||
|
updated, _ = sjson.DeleteBytes(updated, budgetPath)
|
||||||
|
return updated
|
||||||
|
}
|
||||||
|
// If conversion fails, just remove the budget (let API use default)
|
||||||
|
updated, _ := sjson.DeleteBytes(body, budgetPath)
|
||||||
|
return updated
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Gemini 2.5 and other models, normalize the budget value
|
||||||
normalized := NormalizeThinkingBudget(model, int(budget.Int()))
|
normalized := NormalizeThinkingBudget(model, int(budget.Int()))
|
||||||
updated, _ := sjson.SetBytes(body, budgetPath, normalized)
|
updated, _ := sjson.SetBytes(body, budgetPath, normalized)
|
||||||
return updated
|
return updated
|
||||||
@@ -218,34 +440,42 @@ func ApplyReasoningEffortToGeminiCLI(body []byte, effort string) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ConvertThinkingLevelToBudget checks for "generationConfig.thinkingConfig.thinkingLevel"
|
// ConvertThinkingLevelToBudget checks for "generationConfig.thinkingConfig.thinkingLevel"
|
||||||
// and converts it to "thinkingBudget".
|
// and converts it to "thinkingBudget" for Gemini 2.5 models.
|
||||||
// "high" -> 32768
|
// For Gemini 3 models, preserves thinkingLevel as-is (does not convert).
|
||||||
// "low" -> 128
|
// Mappings for Gemini 2.5:
|
||||||
// It removes "thinkingLevel" after conversion.
|
// - "high" -> 32768
|
||||||
func ConvertThinkingLevelToBudget(body []byte) []byte {
|
// - "medium" -> 8192
|
||||||
|
// - "low" -> 1024
|
||||||
|
// - "minimal" -> 512
|
||||||
|
//
|
||||||
|
// It removes "thinkingLevel" after conversion (for Gemini 2.5 only).
|
||||||
|
func ConvertThinkingLevelToBudget(body []byte, model string) []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
|
||||||
|
if IsGemini3Model(model) {
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
level := strings.ToLower(res.String())
|
level := strings.ToLower(res.String())
|
||||||
var budget int
|
var budget int
|
||||||
switch level {
|
switch level {
|
||||||
case "high":
|
case "high":
|
||||||
budget = 32768
|
budget = 32768
|
||||||
|
case "medium":
|
||||||
|
budget = 8192
|
||||||
case "low":
|
case "low":
|
||||||
budget = 128
|
budget = 1024
|
||||||
|
case "minimal":
|
||||||
|
budget = 512
|
||||||
default:
|
default:
|
||||||
// If unknown level, we might just leave it or default.
|
// Unknown level - remove it and let the API use defaults
|
||||||
// User only specified high and low. We'll assume we shouldn't touch it if it's something else,
|
updated, _ := sjson.DeleteBytes(body, levelPath)
|
||||||
// or maybe we should just remove the invalid level?
|
return updated
|
||||||
// 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
|
// Set budget
|
||||||
@@ -262,3 +492,50 @@ func ConvertThinkingLevelToBudget(body []byte) []byte {
|
|||||||
}
|
}
|
||||||
return updated
|
return updated
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConvertThinkingLevelToBudgetCLI checks for "request.generationConfig.thinkingConfig.thinkingLevel"
|
||||||
|
// and converts it to "thinkingBudget" for Gemini 2.5 models.
|
||||||
|
// For Gemini 3 models, preserves thinkingLevel as-is (does not convert).
|
||||||
|
func ConvertThinkingLevelToBudgetCLI(body []byte, model string) []byte {
|
||||||
|
levelPath := "request.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) {
|
||||||
|
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
|
||||||
|
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
|
||||||
|
}
|
||||||
|
return updated
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user