Something went wrong. Try again.
sloproxy
Something went wrong. Try again.
31 kB · 868 lines
Go
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869// Package thinking provides unified thinking configuration processing.package thinking
import ( "strings" "sync"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson")
type pluginProviderApplier struct { owner string priority int applier ProviderApplier}
var providerAppliersMu sync.RWMutex
// nativeProviderAppliers maps built-in provider names to their implementations.var nativeProviderAppliers = map[string]ProviderApplier{ "gemini": nil, "claude": nil, "openai": nil, "codex": nil, "antigravity": nil, "kimi": nil, "xai": nil,}
// pluginProviderAppliers maps plugin-owned provider names to their implementations.var pluginProviderAppliers = map[string]pluginProviderApplier{}
// GetProviderApplier returns the ProviderApplier for the given provider name.// Returns nil if the provider is not registered.func GetProviderApplier(provider string) ProviderApplier { provider = normalizedProviderName(provider) if provider == "" { return nil } providerAppliersMu.RLock() defer providerAppliersMu.RUnlock() if nativeApplier, okNative := nativeProviderAppliers[provider]; okNative { return nativeApplier } return pluginProviderAppliers[provider].applier}
// RegisterProvider registers a provider applier by name.func RegisterProvider(name string, applier ProviderApplier) { name = normalizedProviderName(name) if name == "" { return } providerAppliersMu.Lock() defer providerAppliersMu.Unlock() nativeProviderAppliers[name] = applier}
// RegisterPluginProvider registers a plugin-owned provider applier.func RegisterPluginProvider(owner string, name string, priority int, applier ProviderApplier) bool { owner = strings.TrimSpace(owner) name = normalizedProviderName(name) if owner == "" || name == "" || applier == nil { return false } providerAppliersMu.Lock() defer providerAppliersMu.Unlock() if _, native := nativeProviderAppliers[name]; native { return false } current, exists := pluginProviderAppliers[name] if exists && (current.priority > priority || (current.priority == priority && current.owner <= owner)) { return false } pluginProviderAppliers[name] = pluginProviderApplier{ owner: owner, priority: priority, applier: applier, } return true}
// UnregisterPluginProviders removes all provider appliers owned by one plugin.func UnregisterPluginProviders(owner string) { owner = strings.TrimSpace(owner) if owner == "" { return } providerAppliersMu.Lock() defer providerAppliersMu.Unlock() for provider, record := range pluginProviderAppliers { if record.owner == owner { delete(pluginProviderAppliers, provider) } }}
// ClearPluginProviders removes all plugin-owned provider appliers.func ClearPluginProviders() { providerAppliersMu.Lock() defer providerAppliersMu.Unlock() pluginProviderAppliers = map[string]pluginProviderApplier{}}
func normalizedProviderName(provider string) string { return strings.ToLower(strings.TrimSpace(provider))}
// IsUserDefinedModel reports whether the model is a user-defined model that should// have thinking configuration passed through without validation.//// User-defined models are configured via config file's models[] array// (e.g., openai-compatibility.*.models[], *-api-key.models[]). These models// are marked with UserDefined=true at registration time.//// User-defined models should have their thinking configuration applied directly,// letting the upstream service validate the configuration.func IsUserDefinedModel(modelInfo *registry.ModelInfo) bool { if modelInfo == nil { return true } return modelInfo.UserDefined}
// ApplyThinking applies thinking configuration to a request body.//// This is the unified entry point for all providers. It follows the processing// order defined in FR25: route check → model capability query → config extraction// → validation → application.//// Suffix Priority: When the model name includes a thinking suffix (e.g., "gemini-2.5-pro(8192)"),// the suffix configuration takes priority over any thinking parameters in the request body.// This enables users to override thinking settings via the model name without modifying their// request payload.//// Parameters:// - body: Original request body JSON// - model: Model name, optionally with thinking suffix (e.g., "claude-sonnet-4-5(16384)")// - fromFormat: Source request format (e.g., openai, codex, gemini)// - toFormat: Target provider format for the request body (gemini, antigravity, claude, openai, codex, kimi, xai)// - providerKey: Provider identifier used for registry model lookups (may differ from toFormat, e.g., openrouter -> openai)//// Returns:// - Modified request body JSON with thinking configuration applied// - Error if validation fails (ThinkingError). On error, the original body// is returned (not nil) to enable defensive programming patterns.//// Passthrough behavior (returns original body without error):// - Unknown provider (not in providerAppliers map)// - modelInfo.Thinking is nil (model doesn't support thinking)//// Note: Unknown models (modelInfo is nil) are treated as user-defined models: we skip// validation and still apply the thinking config so the upstream can validate it.//// Example://// // With suffix - suffix config takes priority// result, err := thinking.ApplyThinking(body, "gemini-2.5-pro(8192)", "gemini", "gemini", "gemini")//// // Without suffix - uses body config// result, err := thinking.ApplyThinking(body, "gemini-2.5-pro", "gemini", "gemini", "gemini")func ApplyThinking(body []byte, model string, fromFormat string, toFormat string, providerKey string) ([]byte, error) { summaryConfig := ExtractSummaryConfig(body, toFormat) return applyThinking(body, nil, model, fromFormat, toFormat, providerKey, nil, false, summaryConfig)}
// ApplyThinkingWithSummary applies canonical thinking effort while preserving// summary visibility extracted from the original source request. Callers that// translate before applying thinking must pass the source config explicitly:// a target Claude body can temporarily lack display while disabled thinking is// being rewritten by a model suffix.func ApplyThinkingWithSummary(body []byte, model string, fromFormat string, toFormat string, providerKey string, summaryConfig SummaryConfig) ([]byte, error) { return applyThinking(body, nil, model, fromFormat, toFormat, providerKey, nil, false, summaryConfig)}
// ApplyThinkingWithModelInfo applies thinking with the exact configured model// definition selected for an API-key execution attempt while preserving summary// visibility from the original source body.func ApplyThinkingWithModelInfo(body, sourceBody []byte, model string, fromFormat string, toFormat string, providerKey string, modelInfo *registry.ModelInfo) ([]byte, error) { summaryConfig := ExtractSummaryConfig(sourceBody, fromFormat) if len(sourceBody) == 0 { summaryConfig = ExtractSummaryConfig(body, toFormat) } return ApplyThinkingWithModelInfoAndSummary(body, sourceBody, model, fromFormat, toFormat, providerKey, modelInfo, summaryConfig)}
// ApplyThinkingWithModelInfoAndSummary applies the exact configured model// definition with a summary intent already resolved across source translation// and plugin normalization.func ApplyThinkingWithModelInfoAndSummary(body, sourceBody []byte, model string, fromFormat string, toFormat string, providerKey string, modelInfo *registry.ModelInfo, summaryConfig SummaryConfig) ([]byte, error) { return applyThinking(body, sourceBody, model, fromFormat, toFormat, providerKey, modelInfo, true, summaryConfig)}
func applyThinking(body, sourceBody []byte, model string, fromFormat string, toFormat string, providerKey string, resolvedModelInfo *registry.ModelInfo, modelInfoResolved bool, summaryConfig SummaryConfig) ([]byte, error) { providerFormat := strings.ToLower(strings.TrimSpace(toFormat)) if modelInfoResolved && providerFormat == "openai-response" { providerFormat = "codex" } providerKey = strings.ToLower(strings.TrimSpace(providerKey)) if providerKey == "" { providerKey = providerFormat } fromFormat = strings.ToLower(strings.TrimSpace(fromFormat)) if fromFormat == "" { fromFormat = providerFormat } // Summary visibility is orthogonal to thinking effort. Keep the original // source intent before a suffix-specific applier rewrites provider fields, // then restore it after the canonical effort has been applied. // 1. Route check: Get provider applier applier := GetProviderApplier(providerFormat) if applier == nil { log.WithFields(log.Fields{ "provider": providerFormat, "model": model, }).Debug("thinking: unknown provider, passthrough |") return body, nil }
// 2. Parse suffix and get modelInfo suffixResult := ParseSuffix(model) baseModel := suffixResult.ModelName // Use provider-specific lookup to handle capability differences across providers. modelInfo := resolvedModelInfo if !modelInfoResolved { modelInfo = registry.LookupModelInfo(baseModel, providerKey) }
// 3. Model capability check // Unknown models are treated as user-defined so thinking config can still be applied. // The upstream service is responsible for validating the configuration. if IsUserDefinedModel(modelInfo) { return applyUserDefinedModel(body, modelInfo, fromFormat, providerFormat, providerKey, suffixResult, summaryConfig) } if modelInfo.Thinking == nil { config := extractThinkingConfig(body, providerFormat) if hasThinkingConfig(config) || summaryConfig.Mode != SummaryUnspecified { log.WithFields(log.Fields{ "model": baseModel, "provider": providerFormat, }).Debug("thinking: model does not support thinking, stripping config |") return StripThinkingConfig(body, providerFormat), nil } log.WithFields(log.Fields{ "provider": providerFormat, "model": baseModel, }).Debug("thinking: model does not support thinking, passthrough |") return body, nil }
// 4. Get config: suffix priority over body var config ThinkingConfig if suffixResult.HasSuffix { config = parseSuffixToConfig(suffixResult.RawSuffix, providerFormat, model) log.WithFields(log.Fields{ "provider": providerFormat, "model": model, "mode": config.Mode, "budget": config.Budget, "level": config.Level, }).Debug("thinking: config from model suffix |") } else { if modelInfoResolved && len(sourceBody) > 0 { config = extractSourceThinkingConfig(sourceBody, fromFormat) } if !hasThinkingConfig(config) { config = extractThinkingConfig(body, providerFormat) } if hasThinkingConfig(config) { log.WithFields(log.Fields{ "provider": providerFormat, "model": modelInfo.ID, "mode": config.Mode, "budget": config.Budget, "level": config.Level, }).Debug("thinking: original config from request |") } }
if !hasThinkingConfig(config) { log.WithFields(log.Fields{ "provider": providerFormat, "model": modelInfo.ID, }).Debug("thinking: no config found, passthrough |") if modelInfoResolved && providerFormat == "claude" && fromFormat != providerFormat && ExtractSummaryConfig(sourceBody, fromFormat).Mode == SummaryEnabled { // Registry translation can only see aggregate model capabilities. For a // cross-protocol summary-only request it may have activated adaptive // thinking solely to make display valid. The selected API-key model is // authoritative at execution time, so discard that inferred activation // when the exact model supports only manual extended thinking. Use the // source intent here even if a target normalizer removed display; in that // case the inferred amount must disappear with it. Explicit native Claude // thinking never reaches this cross-protocol branch. body = stripInferredClaudeSummaryActivation(body, modelInfo) } return applySummaryConfigForProvider(body, providerFormat, baseModel, providerKey, modelInfo, summaryConfig), nil } if modelInfoResolved && config.Mode == ModeLevel && modelInfo != nil && modelInfo.Thinking != nil && shouldMapConfiguredHighIntent(fromFormat, providerFormat, modelInfo) { config.Level = mapConfiguredHighIntent(config.Level, modelInfo) }
// 5. Validate and normalize configuration validated, err := ValidateConfig(config, modelInfo, fromFormat, providerFormat, suffixResult.HasSuffix) if err != nil { log.WithFields(log.Fields{ "provider": providerFormat, "model": modelInfo.ID, "error": err.Error(), }).Warn("thinking: validation failed |") // Return original body on validation failure (defensive programming). // This ensures callers who ignore the error won't receive nil body. // The upstream service will decide how to handle the unmodified request. return body, err }
// Defensive check: ValidateConfig should never return (nil, nil) if validated == nil { log.WithFields(log.Fields{ "provider": providerFormat, "model": modelInfo.ID, }).Warn("thinking: ValidateConfig returned nil config without error, passthrough |") return body, nil }
log.WithFields(log.Fields{ "provider": providerFormat, "model": modelInfo.ID, "mode": validated.Mode, "budget": validated.Budget, "level": validated.Level, }).Debug("thinking: processed config to apply |")
// 6. Apply configuration using provider-specific applier, then restore the // target summary intent that was explicit before suffix processing. applied, err := applier.Apply(body, *validated, modelInfo) if err != nil { return applied, err } // A fully disabled amount takes precedence over visibility. Re-applying a // summary-only field can recreate an otherwise removed provider config and // make a default-on model think again. if thinkingIsFullyDisabled(*validated) { return applied, nil } return applySummaryConfigForProvider(applied, providerFormat, baseModel, providerKey, modelInfo, summaryConfig), nil}
func thinkingIsFullyDisabled(config ThinkingConfig) bool { return config.Mode == ModeNone && config.Budget == 0 && config.Level == ""}
func shouldMapConfiguredHighIntent(fromFormat, toFormat string, modelInfo *registry.ModelInfo) bool { fromFormat = strings.ToLower(strings.TrimSpace(fromFormat)) toFormat = strings.ToLower(strings.TrimSpace(toFormat)) if fromFormat != toFormat { return true } if modelInfo == nil { return false } modelType := strings.ToLower(strings.TrimSpace(modelInfo.Type)) return modelType != "" && !isSameProviderFamily(toFormat, modelType)}
func mapConfiguredHighIntent(level ThinkingLevel, modelInfo *registry.ModelInfo) ThinkingLevel { if modelInfo == nil || modelInfo.Thinking == nil || len(modelInfo.Thinking.Levels) == 0 { return level } level = ThinkingLevel(strings.ToLower(strings.TrimSpace(string(level)))) var candidates []ThinkingLevel switch level { case LevelXHigh: candidates = []ThinkingLevel{LevelXHigh, LevelMax, LevelHigh} case LevelMax: candidates = []ThinkingLevel{LevelMax, LevelXHigh, LevelHigh} default: return level } for _, candidate := range candidates { if isLevelSupported(string(candidate), modelInfo.Thinking.Levels) { return candidate } } return level}
func extractSourceThinkingConfig(body []byte, provider string) ThinkingConfig { provider = strings.ToLower(strings.TrimSpace(provider)) if provider == "openai-response" { return extractCodexConfig(body) } return extractThinkingConfig(body, provider)}
// parseSuffixToConfig converts a raw suffix string to ThinkingConfig.//// Parsing priority:// 1. Special values: "none" → ModeNone, "auto"/"-1" → ModeAuto// 2. Level names: "minimal", "low", "medium", "high", "xhigh" → ModeLevel// 3. Numeric values: positive integers → ModeBudget, 0 → ModeNone//// If none of the above match, returns empty ThinkingConfig (treated as no config).func parseSuffixToConfig(rawSuffix, provider, model string) ThinkingConfig { // 1. Try special values first (none, auto, -1) if mode, ok := ParseSpecialSuffix(rawSuffix); ok { switch mode { case ModeNone: return ThinkingConfig{Mode: ModeNone, Budget: 0} case ModeAuto: return ThinkingConfig{Mode: ModeAuto, Budget: -1} } }
// 2. Try level parsing (minimal, low, medium, high, xhigh) if level, ok := ParseLevelSuffix(rawSuffix); ok { return ThinkingConfig{Mode: ModeLevel, Level: level} }
// 3. Try numeric parsing if budget, ok := ParseNumericSuffix(rawSuffix); ok { if budget == 0 { return ThinkingConfig{Mode: ModeNone, Budget: 0} } return ThinkingConfig{Mode: ModeBudget, Budget: budget} }
// Unknown suffix format - return empty config log.WithFields(log.Fields{ "provider": provider, "model": model, "raw_suffix": rawSuffix, }).Debug("thinking: unknown suffix format, treating as no config |") return ThinkingConfig{}}
// applyUserDefinedModel applies thinking configuration for user-defined models// without ThinkingSupport validation.func applyUserDefinedModel(body []byte, modelInfo *registry.ModelInfo, fromFormat, toFormat, providerKey string, suffixResult SuffixResult, summaryConfig SummaryConfig) ([]byte, error) { // Get model ID for logging modelID := "" if modelInfo != nil { modelID = modelInfo.ID } else { modelID = suffixResult.ModelName }
// Get config: suffix priority over body var config ThinkingConfig if suffixResult.HasSuffix { config = parseSuffixToConfig(suffixResult.RawSuffix, toFormat, modelID) log.WithFields(log.Fields{ "provider": toFormat, "model": modelID, "mode": config.Mode, "budget": config.Budget, "level": config.Level, }).Debug("thinking: config from model suffix |") } else { config = extractThinkingConfig(body, fromFormat) if !hasThinkingConfig(config) && fromFormat != toFormat { config = extractThinkingConfig(body, toFormat) } if hasThinkingConfig(config) { log.WithFields(log.Fields{ "provider": toFormat, "model": modelID, "mode": config.Mode, "budget": config.Budget, "level": config.Level, }).Debug("thinking: original config from request |") } }
if !hasThinkingConfig(config) { log.WithFields(log.Fields{ "model": modelID, "provider": toFormat, }).Debug("thinking: user-defined model, passthrough (no config) |") return applySummaryConfigForProvider(body, toFormat, modelID, providerKey, modelInfo, summaryConfig), nil }
applier := GetProviderApplier(toFormat) if applier == nil { log.WithFields(log.Fields{ "model": modelID, "provider": toFormat, }).Debug("thinking: user-defined model, passthrough (unknown provider) |") return body, nil }
config = normalizeUserDefinedConfig(config, fromFormat, toFormat) log.WithFields(log.Fields{ "provider": toFormat, "model": modelID, "mode": config.Mode, "budget": config.Budget, "level": config.Level, }).Debug("thinking: processed config to apply |") applied, err := applier.Apply(body, config, modelInfo) if err != nil { return applied, err } if thinkingIsFullyDisabled(config) { return applied, nil } return applySummaryConfigForProvider(applied, toFormat, modelID, providerKey, modelInfo, summaryConfig), nil}
func normalizeUserDefinedConfig(config ThinkingConfig, fromFormat, toFormat string) ThinkingConfig { if config.Mode != ModeLevel { return config } if toFormat == "claude" { return config } if !isBudgetCapableProvider(toFormat) { return config } budget, ok := ConvertLevelToBudget(string(config.Level)) if !ok { return config } config.Mode = ModeBudget config.Budget = budget config.Level = "" return config}
// extractThinkingConfig extracts provider-specific thinking config from request body.func extractThinkingConfig(body []byte, provider string) ThinkingConfig { if len(body) == 0 || !gjson.ValidBytes(body) { return ThinkingConfig{} }
switch provider { case "claude": return extractClaudeConfig(body) case "gemini", "antigravity": return extractGeminiConfig(body, provider) case "interactions": return extractInteractionsConfig(body) case "openai": return extractOpenAIConfig(body) case "codex", "xai": return extractCodexConfig(body) case "kimi": return extractKimiConfig(body) default: return ThinkingConfig{} }}
func hasThinkingConfig(config ThinkingConfig) bool { return config.Mode != ModeBudget || config.Budget != 0 || config.Level != ""}
// ExtractReasoningEffort returns the request's thinking setting as a canonical// reasoning_effort label for usage logging. Model suffixes have the same// priority as ApplyThinking: a valid suffix overrides body fields.func ExtractReasoningEffort(body []byte, provider, model string) string { if effort := reasoningEffortFromSuffix(ParseSuffix(model)); effort != "" { return effort }
provider = strings.ToLower(strings.TrimSpace(provider)) config := extractThinkingConfig(body, provider) if !hasThinkingConfig(config) { switch provider { case "openai-response": config = extractCodexConfig(body) case "openai": config = extractCodexConfig(body) } } return reasoningEffortFromConfig(config)}
// ExtractTranslatedReasoningEffort returns the final provider payload's thinking// setting as a canonical reasoning_effort label for usage logging.func ExtractTranslatedReasoningEffort(body []byte, provider string) string { provider = strings.ToLower(strings.TrimSpace(provider)) config := extractThinkingConfig(body, provider) if !hasThinkingConfig(config) { switch provider { case "openai", "openai-response": config = extractCodexConfig(body) if !hasThinkingConfig(config) { config = extractOpenAIConfig(body) } } } return reasoningEffortFromConfig(config)}
func reasoningEffortFromSuffix(suffix SuffixResult) string { if !suffix.HasSuffix { return "" } return reasoningEffortFromConfig(parseSuffixToConfig(suffix.RawSuffix, "", suffix.ModelName))}
func reasoningEffortFromConfig(config ThinkingConfig) string { if !hasThinkingConfig(config) { return "" } switch config.Mode { case ModeNone: return string(LevelNone) case ModeAuto: return string(LevelAuto) case ModeLevel: return strings.ToLower(strings.TrimSpace(string(config.Level))) case ModeBudget: level, ok := ConvertBudgetToLevel(config.Budget) if !ok { return "" } return level default: return "" }}
// extractClaudeConfig extracts thinking configuration from Claude format request body.//// Claude API format:// - thinking.type: "enabled" or "disabled"// - thinking.budget_tokens: integer (-1=auto, 0=disabled, >0=budget)//// Priority: thinking.type="disabled" takes precedence over budget_tokens.// When type="enabled" without budget_tokens, returns ModeAuto to indicate// the user wants thinking enabled but didn't specify a budget.func extractClaudeConfig(body []byte) ThinkingConfig { thinkingType := gjson.GetBytes(body, "thinking.type").String() if thinkingType == "disabled" { return ThinkingConfig{Mode: ModeNone, Budget: 0} } if thinkingType == "adaptive" || thinkingType == "auto" { // Claude adaptive thinking uses output_config.effort (low/medium/high/max). // We only treat it as a thinking config when effort is explicitly present; // otherwise we passthrough and let upstream defaults apply. if effort := gjson.GetBytes(body, "output_config.effort"); effort.Exists() && effort.Type == gjson.String { value := strings.ToLower(strings.TrimSpace(effort.String())) if value == "" { return ThinkingConfig{} } switch value { case "none": return ThinkingConfig{Mode: ModeNone, Budget: 0} case "auto": return ThinkingConfig{Mode: ModeAuto, Budget: -1} default: return ThinkingConfig{Mode: ModeLevel, Level: ThinkingLevel(value)} } } return ThinkingConfig{} }
// Check budget_tokens if budget := gjson.GetBytes(body, "thinking.budget_tokens"); budget.Exists() { value := int(budget.Int()) switch value { case 0: return ThinkingConfig{Mode: ModeNone, Budget: 0} case -1: return ThinkingConfig{Mode: ModeAuto, Budget: -1} default: return ThinkingConfig{Mode: ModeBudget, Budget: value} } }
// If type="enabled" but no budget_tokens, treat as auto (user wants thinking but no budget specified) if thinkingType == "enabled" { return ThinkingConfig{Mode: ModeAuto, Budget: -1} }
return ThinkingConfig{}}
// extractGeminiConfig extracts thinking configuration from Gemini format request body.//// Gemini API format:// - generationConfig.thinkingConfig.thinkingLevel: "none", "auto", or level name (Gemini 3)// - generationConfig.thinkingConfig.thinkingBudget: integer (Gemini 2.5)//// For antigravity providers, the path is prefixed with "request.".//// Priority: thinkingLevel is checked first (Gemini 3 format), then thinkingBudget (Gemini 2.5 format).// This allows newer Gemini 3 level-based configs to take precedence.func extractGeminiConfig(body []byte, provider string) ThinkingConfig { prefix := "generationConfig.thinkingConfig" if provider == "antigravity" { prefix = "request.generationConfig.thinkingConfig" }
// Check thinkingLevel first (Gemini 3 format takes precedence) level := gjson.GetBytes(body, prefix+".thinkingLevel") if !level.Exists() { // Google official Gemini Python SDK sends snake_case field names level = gjson.GetBytes(body, prefix+".thinking_level") } if level.Exists() { value := level.String() switch value { case "none": return ThinkingConfig{Mode: ModeNone, Budget: 0} case "auto": return ThinkingConfig{Mode: ModeAuto, Budget: -1} default: return ThinkingConfig{Mode: ModeLevel, Level: ThinkingLevel(value)} } }
// Check thinkingBudget (Gemini 2.5 format) budget := gjson.GetBytes(body, prefix+".thinkingBudget") if !budget.Exists() { // Google official Gemini Python SDK sends snake_case field names budget = gjson.GetBytes(body, prefix+".thinking_budget") } if budget.Exists() { value := int(budget.Int()) switch value { case 0: return ThinkingConfig{Mode: ModeNone, Budget: 0} case -1: return ThinkingConfig{Mode: ModeAuto, Budget: -1} default: return ThinkingConfig{Mode: ModeBudget, Budget: value} } }
return ThinkingConfig{}}
func extractInteractionsConfig(body []byte) ThinkingConfig { for _, path := range []string{ "generation_config.thinking_level", "generation_config.thinkingLevel", "generation_config.thinking_config.thinking_level", "generation_config.thinking_config.thinkingLevel", "generation_config.thinkingConfig.thinking_level", "generation_config.thinkingConfig.thinkingLevel", } { level := gjson.GetBytes(body, path) if !level.Exists() { continue } value := strings.ToLower(strings.TrimSpace(level.String())) switch value { case "none": return ThinkingConfig{Mode: ModeNone, Budget: 0} case "auto": return ThinkingConfig{Mode: ModeAuto, Budget: -1} default: return ThinkingConfig{Mode: ModeLevel, Level: ThinkingLevel(value)} } }
for _, path := range []string{ "generation_config.thinking_budget", "generation_config.thinkingBudget", "generation_config.thinking_config.thinking_budget", "generation_config.thinking_config.thinkingBudget", "generation_config.thinkingConfig.thinking_budget", "generation_config.thinkingConfig.thinkingBudget", } { budget := gjson.GetBytes(body, path) if !budget.Exists() { continue } value := int(budget.Int()) switch value { case 0: return ThinkingConfig{Mode: ModeNone, Budget: 0} case -1: return ThinkingConfig{Mode: ModeAuto, Budget: -1} default: return ThinkingConfig{Mode: ModeBudget, Budget: value} } }
return ThinkingConfig{}}
// extractOpenAIConfig extracts thinking configuration from OpenAI format request body.//// OpenAI API format:// - reasoning_effort: "none", "low", "medium", "high" (discrete levels)//// OpenAI uses level-based thinking configuration only, no numeric budget support.// The "none" value is treated specially to return ModeNone.func extractOpenAIConfig(body []byte) ThinkingConfig { // Check reasoning_effort (OpenAI Chat Completions format) if effort := gjson.GetBytes(body, "reasoning_effort"); effort.Exists() { value := effort.String() if value == "none" { return ThinkingConfig{Mode: ModeNone, Budget: 0} } return ThinkingConfig{Mode: ModeLevel, Level: ThinkingLevel(value)} }
return ThinkingConfig{}}
// extractKimiConfig extracts Kimi's native thinking object while retaining// reasoning_effort as a legacy input fallback.//// Native fields take precedence over reasoning_effort. In particular,// thinking.type="enabled" without an explicit effort means "use the upstream// default" and therefore returns an empty config so ApplyThinking preserves the// request unchanged instead of interpreting it as CPA's ModeAuto.func extractKimiConfig(body []byte) ThinkingConfig { thinkingType := gjson.GetBytes(body, "thinking.type") if thinkingType.Exists() { switch strings.ToLower(strings.TrimSpace(thinkingType.String())) { case "disabled": return ThinkingConfig{Mode: ModeNone, Budget: 0} case "enabled": if !gjson.GetBytes(body, "thinking.effort").Exists() { return ThinkingConfig{} } } }
if effort := gjson.GetBytes(body, "thinking.effort"); effort.Exists() { value := strings.ToLower(strings.TrimSpace(effort.String())) switch value { case "": return ThinkingConfig{} case "none": return ThinkingConfig{Mode: ModeNone, Budget: 0} case "auto": return ThinkingConfig{Mode: ModeAuto, Budget: -1} default: return ThinkingConfig{Mode: ModeLevel, Level: ThinkingLevel(value)} } }
// An explicit native thinking object without an effort should be left for // the Kimi upstream to interpret and must not be overridden by the legacy // field. if thinkingType.Exists() { return ThinkingConfig{} }
return extractOpenAIConfig(body)}
// extractCodexConfig extracts thinking configuration from Codex format request body.//// Codex API format (OpenAI Responses API):// - reasoning.effort: "none", "low", "medium", "high"//// This is similar to OpenAI but uses nested field "reasoning.effort" instead of "reasoning_effort".func extractCodexConfig(body []byte) ThinkingConfig { // Check reasoning.effort (Codex / OpenAI Responses API format) if effort := gjson.GetBytes(body, "reasoning.effort"); effort.Exists() { value := effort.String() if value == "none" { return ThinkingConfig{Mode: ModeNone, Budget: 0} } return ThinkingConfig{Mode: ModeLevel, Level: ThinkingLevel(value)} }
return ThinkingConfig{}}