diff --git a/internal/runtime/executor/antigravity_executor.go b/internal/runtime/executor/antigravity_executor.go index 234b06cb..46b2d4ea 100644 --- a/internal/runtime/executor/antigravity_executor.go +++ b/internal/runtime/executor/antigravity_executor.go @@ -1467,7 +1467,7 @@ func normalizeAntigravityThinking(model string, payload []byte, isClaude bool) [ return payload } raw := int(budget.Int()) - normalized := thinking.ClampBudget(raw, modelInfo.Thinking.Min, modelInfo.Thinking.Max) + normalized := thinking.ClampBudget(raw, modelInfo, "antigravity") if isClaude { effectiveMax, setDefaultMax := antigravityEffectiveMaxTokens(model, payload) diff --git a/internal/thinking/apply.go b/internal/thinking/apply.go index 0b26ca0b..ce210dfb 100644 --- a/internal/thinking/apply.go +++ b/internal/thinking/apply.go @@ -84,7 +84,10 @@ func ApplyThinking(body []byte, model string, provider string) ([]byte, error) { // 1. Route check: Get provider applier applier := GetProviderApplier(provider) if applier == nil { - log.WithField("provider", provider).Debug("thinking: unknown provider, passthrough") + log.WithFields(log.Fields{ + "provider": provider, + "model": model, + }).Debug("thinking: unknown provider, passthrough") return body, nil } @@ -108,14 +111,17 @@ func ApplyThinking(body []byte, model string, provider string) ([]byte, error) { }).Debug("thinking: model does not support thinking, stripping config") return StripThinkingConfig(body, provider), nil } - log.WithField("model", baseModel).Debug("thinking: model does not support thinking, passthrough") + log.WithFields(log.Fields{ + "provider": provider, + "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) + config = parseSuffixToConfig(suffixResult.RawSuffix, provider, model) log.WithFields(log.Fields{ "provider": provider, "model": model, @@ -125,13 +131,15 @@ func ApplyThinking(body []byte, model string, provider string) ([]byte, error) { }).Debug("thinking: config from model suffix") } else { config = extractThinkingConfig(body, provider) - log.WithFields(log.Fields{ - "provider": provider, - "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": provider, + "model": modelInfo.ID, + "mode": config.Mode, + "budget": config.Budget, + "level": config.Level, + }).Debug("thinking: original config from request") + } } if !hasThinkingConfig(config) { @@ -143,7 +151,7 @@ func ApplyThinking(body []byte, model string, provider string) ([]byte, error) { } // 5. Validate and normalize configuration - validated, err := ValidateConfig(config, modelInfo.Thinking) + validated, err := ValidateConfig(config, modelInfo, provider) if err != nil { log.WithFields(log.Fields{ "provider": provider, @@ -185,7 +193,7 @@ func ApplyThinking(body []byte, model string, provider string) ([]byte, error) { // 3. Numeric values: positive integers → ModeBudget, 0 → ModeNone // // If none of the above match, returns empty ThinkingConfig (treated as no config). -func parseSuffixToConfig(rawSuffix string) ThinkingConfig { +func parseSuffixToConfig(rawSuffix, provider, model string) ThinkingConfig { // 1. Try special values first (none, auto, -1) if mode, ok := ParseSpecialSuffix(rawSuffix); ok { switch mode { @@ -210,7 +218,11 @@ func parseSuffixToConfig(rawSuffix string) ThinkingConfig { } // Unknown suffix format - return empty config - log.WithField("raw_suffix", rawSuffix).Debug("thinking: unknown suffix format, treating as no config") + log.WithFields(log.Fields{ + "provider": provider, + "model": model, + "raw_suffix": rawSuffix, + }).Debug("thinking: unknown suffix format, treating as no config") return ThinkingConfig{} } @@ -228,7 +240,7 @@ func applyUserDefinedModel(body []byte, modelInfo *registry.ModelInfo, provider // Get config: suffix priority over body var config ThinkingConfig if suffixResult.HasSuffix { - config = parseSuffixToConfig(suffixResult.RawSuffix) + config = parseSuffixToConfig(suffixResult.RawSuffix, provider, modelID) } else { config = extractThinkingConfig(body, provider) } diff --git a/internal/thinking/validate.go b/internal/thinking/validate.go index 886f3161..3a92ec12 100644 --- a/internal/thinking/validate.go +++ b/internal/thinking/validate.go @@ -9,81 +9,59 @@ import ( log "github.com/sirupsen/logrus" ) -// ClampBudget clamps a budget value to the specified range [min, max]. -// -// This function ensures budget values stay within model-supported bounds. -// When clamping occurs, a Debug-level log is recorded. -// -// Special handling: -// - Auto value (-1) passes through without clamping -// - Values below min are clamped to min -// - Values above max are clamped to max -// -// Parameters: -// - value: The budget value to clamp -// - min: Minimum allowed budget (inclusive) -// - max: Maximum allowed budget (inclusive) -// -// Returns: -// - The clamped budget value (min ≤ result ≤ max, or -1 for auto) +// ClampBudget clamps a budget value to the model's supported range. // // Logging: -// - Debug level when value is clamped (either to min or max) -// - Fields: original_value, clamped_to, min, max -func ClampBudget(value, min, max int) int { - // Auto value (-1) passes through without clamping +// - Warn when value=0 but ZeroAllowed=false +// - Debug when value is clamped to min/max +// +// Fields: provider, model, original_value, clamped_to, min, max +func ClampBudget(value int, modelInfo *registry.ModelInfo, provider string) int { + model := "unknown" + support := (*registry.ThinkingSupport)(nil) + if modelInfo != nil { + if modelInfo.ID != "" { + model = modelInfo.ID + } + support = modelInfo.Thinking + } + if support == nil { + return value + } + + // Auto value (-1) passes through without clamping. if value == -1 { return value } - // Clamp to min if below - if value < min { - logClamp(value, min, min, max) - return min - } - - // Clamp to max if above - if value > max { - logClamp(value, max, min, max) - return max - } - - // Within range, return original - return value -} - -// ClampBudgetWithZeroCheck clamps a budget value to the specified range [min, max] -// while honoring the ZeroAllowed constraint. -// -// This function extends ClampBudget with ZeroAllowed boundary handling. -// When zeroAllowed is false and value is 0, the value is clamped to min and logged. -// -// Parameters: -// - value: The budget value to clamp -// - min: Minimum allowed budget (inclusive) -// - max: Maximum allowed budget (inclusive) -// - zeroAllowed: Whether 0 (thinking disabled) is allowed -// -// Returns: -// - The clamped budget value (min ≤ result ≤ max, or -1 for auto) -// -// Logging: -// - Warn level when zeroAllowed=false and value=0 (zero not allowed for model) -// - Fields: original_value, clamped_to, reason -func ClampBudgetWithZeroCheck(value, min, max int, zeroAllowed bool) int { - if value == 0 { - if zeroAllowed { - return 0 - } + min := support.Min + max := support.Max + if value == 0 && !support.ZeroAllowed { log.WithFields(log.Fields{ - "clamped_to": min, - "min": min, - "max": max, + "provider": provider, + "model": model, + "original_value": value, + "clamped_to": min, + "min": min, + "max": max, }).Warn("thinking: budget zero not allowed") return min } - return ClampBudget(value, min, max) + // Some models are level-only and do not define numeric budget ranges. + if min == 0 && max == 0 { + return value + } + + if value < min { + logClamp(provider, model, value, min, min, max) + return min + } + if value > max { + logClamp(provider, model, value, max, min, max) + return max + } + return value } // ValidateConfig validates a thinking configuration against model capabilities. @@ -106,16 +84,26 @@ func ClampBudgetWithZeroCheck(value, min, max int, zeroAllowed bool) int { // - Budget-only model + Level config → Level converted to Budget // - Level-only model + Budget config → Budget converted to Level // - Hybrid model → preserve original format -func ValidateConfig(config ThinkingConfig, support *registry.ThinkingSupport) (*ThinkingConfig, error) { +func ValidateConfig(config ThinkingConfig, modelInfo *registry.ModelInfo, provider string) (*ThinkingConfig, error) { normalized := config + + model := "unknown" + support := (*registry.ThinkingSupport)(nil) + if modelInfo != nil { + if modelInfo.ID != "" { + model = modelInfo.ID + } + support = modelInfo.Thinking + } + if support == nil { if config.Mode != ModeNone { - return nil, NewThinkingErrorWithModel(ErrThinkingNotSupported, "thinking not supported for this model", "unknown") + return nil, NewThinkingErrorWithModel(ErrThinkingNotSupported, "thinking not supported for this model", model) } return &normalized, nil } - capability := detectModelCapability(®istry.ModelInfo{Thinking: support}) + capability := detectModelCapability(modelInfo) switch capability { case CapabilityBudgetOnly: if normalized.Mode == ModeLevel { @@ -168,13 +156,12 @@ func ValidateConfig(config ThinkingConfig, support *registry.ThinkingSupport) (* // Convert ModeAuto to mid-range if dynamic not allowed if normalized.Mode == ModeAuto && !support.DynamicAllowed { - normalized = convertAutoToMidRange(normalized, support) + normalized = convertAutoToMidRange(normalized, support, provider, model) } switch normalized.Mode { case ModeBudget, ModeAuto, ModeNone: - clamped := ClampBudgetWithZeroCheck(normalized.Budget, support.Min, support.Max, support.ZeroAllowed) - normalized.Budget = clamped + normalized.Budget = ClampBudget(normalized.Budget, modelInfo, provider) } // ModeNone with clamped Budget > 0: set Level to lowest for Level-only/Hybrid models @@ -213,17 +200,18 @@ func normalizeLevels(levels []string) []string { // Logging: // - Debug level when conversion occurs // - Fields: original_mode, clamped_to, reason -func convertAutoToMidRange(config ThinkingConfig, support *registry.ThinkingSupport) ThinkingConfig { +func convertAutoToMidRange(config ThinkingConfig, support *registry.ThinkingSupport, provider, model string) ThinkingConfig { // For level-only models (has Levels but no Min/Max range), use ModeLevel with medium if len(support.Levels) > 0 && support.Min == 0 && support.Max == 0 { config.Mode = ModeLevel config.Level = LevelMedium config.Budget = 0 log.WithFields(log.Fields{ + "provider": provider, + "model": model, "original_mode": "auto", "clamped_to": string(LevelMedium), - "reason": "dynamic_not_allowed_level_only", - }).Debug("thinking mode converted: dynamic not allowed, using medium level") + }).Debug("thinking: mode converted: dynamic not allowed, using medium level") return config } @@ -240,16 +228,19 @@ func convertAutoToMidRange(config ThinkingConfig, support *registry.ThinkingSupp config.Budget = mid } log.WithFields(log.Fields{ + "provider": provider, + "model": model, "original_mode": "auto", "clamped_to": config.Budget, - "reason": "dynamic_not_allowed", - }).Debug("thinking mode converted: dynamic not allowed") + }).Debug("thinking: mode converted: dynamic not allowed") return config } // logClamp logs a debug message when budget clamping occurs. -func logClamp(original, clampedTo, min, max int) { +func logClamp(provider, model string, original, clampedTo, min, max int) { log.WithFields(log.Fields{ + "provider": provider, + "model": model, "original_value": original, "min": min, "max": max, diff --git a/internal/translator/claude/gemini/claude_gemini_request.go b/internal/translator/claude/gemini/claude_gemini_request.go index 8c5b1095..89857693 100644 --- a/internal/translator/claude/gemini/claude_gemini_request.go +++ b/internal/translator/claude/gemini/claude_gemini_request.go @@ -16,7 +16,6 @@ import ( "github.com/google/uuid" "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" - "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/tidwall/gjson" "github.com/tidwall/sjson" @@ -123,8 +122,7 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream // Check for thinkingBudget first - if present, enable thinking with budget if thinkingBudget := thinkingConfig.Get("thinkingBudget"); thinkingBudget.Exists() && thinkingBudget.Int() > 0 { out, _ = sjson.Set(out, "thinking.type", "enabled") - normalizedBudget := thinking.ClampBudget(int(thinkingBudget.Int()), modelInfo.Thinking.Min, modelInfo.Thinking.Max) - out, _ = sjson.Set(out, "thinking.budget_tokens", normalizedBudget) + out, _ = sjson.Set(out, "thinking.budget_tokens", thinkingBudget.Int()) } else if includeThoughts := thinkingConfig.Get("include_thoughts"); includeThoughts.Exists() && includeThoughts.Type == gjson.True { // Fallback to include_thoughts if no budget specified out, _ = sjson.Set(out, "thinking.type", "enabled")