refactor(thinking): improve budget clamping and logging with provider/model context

This commit is contained in:
hkfires
2026-01-15 11:29:53 +08:00
parent 1fbbba6f59
commit 5a77b7728e
4 changed files with 95 additions and 94 deletions

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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(&registry.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,

View File

@@ -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")