mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-02 04:20:50 +08:00
refactor(thinking): improve budget clamping and logging with provider/model context
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user