mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-18 20:30:51 +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
|
return payload
|
||||||
}
|
}
|
||||||
raw := int(budget.Int())
|
raw := int(budget.Int())
|
||||||
normalized := thinking.ClampBudget(raw, modelInfo.Thinking.Min, modelInfo.Thinking.Max)
|
normalized := thinking.ClampBudget(raw, modelInfo, "antigravity")
|
||||||
|
|
||||||
if isClaude {
|
if isClaude {
|
||||||
effectiveMax, setDefaultMax := antigravityEffectiveMaxTokens(model, payload)
|
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
|
// 1. Route check: Get provider applier
|
||||||
applier := GetProviderApplier(provider)
|
applier := GetProviderApplier(provider)
|
||||||
if applier == nil {
|
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
|
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")
|
}).Debug("thinking: model does not support thinking, stripping config")
|
||||||
return StripThinkingConfig(body, provider), nil
|
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
|
return body, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Get config: suffix priority over body
|
// 4. Get config: suffix priority over body
|
||||||
var config ThinkingConfig
|
var config ThinkingConfig
|
||||||
if suffixResult.HasSuffix {
|
if suffixResult.HasSuffix {
|
||||||
config = parseSuffixToConfig(suffixResult.RawSuffix)
|
config = parseSuffixToConfig(suffixResult.RawSuffix, provider, model)
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"provider": provider,
|
"provider": provider,
|
||||||
"model": model,
|
"model": model,
|
||||||
@@ -125,13 +131,15 @@ func ApplyThinking(body []byte, model string, provider string) ([]byte, error) {
|
|||||||
}).Debug("thinking: config from model suffix")
|
}).Debug("thinking: config from model suffix")
|
||||||
} else {
|
} else {
|
||||||
config = extractThinkingConfig(body, provider)
|
config = extractThinkingConfig(body, provider)
|
||||||
log.WithFields(log.Fields{
|
if hasThinkingConfig(config) {
|
||||||
"provider": provider,
|
log.WithFields(log.Fields{
|
||||||
"model": modelInfo.ID,
|
"provider": provider,
|
||||||
"mode": config.Mode,
|
"model": modelInfo.ID,
|
||||||
"budget": config.Budget,
|
"mode": config.Mode,
|
||||||
"level": config.Level,
|
"budget": config.Budget,
|
||||||
}).Debug("thinking: original config from request")
|
"level": config.Level,
|
||||||
|
}).Debug("thinking: original config from request")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !hasThinkingConfig(config) {
|
if !hasThinkingConfig(config) {
|
||||||
@@ -143,7 +151,7 @@ func ApplyThinking(body []byte, model string, provider string) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 5. Validate and normalize configuration
|
// 5. Validate and normalize configuration
|
||||||
validated, err := ValidateConfig(config, modelInfo.Thinking)
|
validated, err := ValidateConfig(config, modelInfo, provider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"provider": provider,
|
"provider": provider,
|
||||||
@@ -185,7 +193,7 @@ func ApplyThinking(body []byte, model string, provider string) ([]byte, error) {
|
|||||||
// 3. Numeric values: positive integers → ModeBudget, 0 → ModeNone
|
// 3. Numeric values: positive integers → ModeBudget, 0 → ModeNone
|
||||||
//
|
//
|
||||||
// If none of the above match, returns empty ThinkingConfig (treated as no config).
|
// 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)
|
// 1. Try special values first (none, auto, -1)
|
||||||
if mode, ok := ParseSpecialSuffix(rawSuffix); ok {
|
if mode, ok := ParseSpecialSuffix(rawSuffix); ok {
|
||||||
switch mode {
|
switch mode {
|
||||||
@@ -210,7 +218,11 @@ func parseSuffixToConfig(rawSuffix string) ThinkingConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Unknown suffix format - return empty config
|
// 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{}
|
return ThinkingConfig{}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +240,7 @@ func applyUserDefinedModel(body []byte, modelInfo *registry.ModelInfo, provider
|
|||||||
// Get config: suffix priority over body
|
// Get config: suffix priority over body
|
||||||
var config ThinkingConfig
|
var config ThinkingConfig
|
||||||
if suffixResult.HasSuffix {
|
if suffixResult.HasSuffix {
|
||||||
config = parseSuffixToConfig(suffixResult.RawSuffix)
|
config = parseSuffixToConfig(suffixResult.RawSuffix, provider, modelID)
|
||||||
} else {
|
} else {
|
||||||
config = extractThinkingConfig(body, provider)
|
config = extractThinkingConfig(body, provider)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,81 +9,59 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ClampBudget clamps a budget value to the specified range [min, max].
|
// ClampBudget clamps a budget value to the model's supported range.
|
||||||
//
|
|
||||||
// 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)
|
|
||||||
//
|
//
|
||||||
// Logging:
|
// Logging:
|
||||||
// - Debug level when value is clamped (either to min or max)
|
// - Warn when value=0 but ZeroAllowed=false
|
||||||
// - Fields: original_value, clamped_to, min, max
|
// - Debug when value is clamped to min/max
|
||||||
func ClampBudget(value, min, max int) int {
|
//
|
||||||
// Auto value (-1) passes through without clamping
|
// 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 {
|
if value == -1 {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clamp to min if below
|
min := support.Min
|
||||||
if value < min {
|
max := support.Max
|
||||||
logClamp(value, min, min, max)
|
if value == 0 && !support.ZeroAllowed {
|
||||||
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
|
|
||||||
}
|
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"clamped_to": min,
|
"provider": provider,
|
||||||
"min": min,
|
"model": model,
|
||||||
"max": max,
|
"original_value": value,
|
||||||
|
"clamped_to": min,
|
||||||
|
"min": min,
|
||||||
|
"max": max,
|
||||||
}).Warn("thinking: budget zero not allowed")
|
}).Warn("thinking: budget zero not allowed")
|
||||||
return min
|
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.
|
// 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
|
// - Budget-only model + Level config → Level converted to Budget
|
||||||
// - Level-only model + Budget config → Budget converted to Level
|
// - Level-only model + Budget config → Budget converted to Level
|
||||||
// - Hybrid model → preserve original format
|
// - 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
|
normalized := config
|
||||||
|
|
||||||
|
model := "unknown"
|
||||||
|
support := (*registry.ThinkingSupport)(nil)
|
||||||
|
if modelInfo != nil {
|
||||||
|
if modelInfo.ID != "" {
|
||||||
|
model = modelInfo.ID
|
||||||
|
}
|
||||||
|
support = modelInfo.Thinking
|
||||||
|
}
|
||||||
|
|
||||||
if support == nil {
|
if support == nil {
|
||||||
if config.Mode != ModeNone {
|
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
|
return &normalized, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
capability := detectModelCapability(®istry.ModelInfo{Thinking: support})
|
capability := detectModelCapability(modelInfo)
|
||||||
switch capability {
|
switch capability {
|
||||||
case CapabilityBudgetOnly:
|
case CapabilityBudgetOnly:
|
||||||
if normalized.Mode == ModeLevel {
|
if normalized.Mode == ModeLevel {
|
||||||
@@ -168,13 +156,12 @@ func ValidateConfig(config ThinkingConfig, support *registry.ThinkingSupport) (*
|
|||||||
|
|
||||||
// Convert ModeAuto to mid-range if dynamic not allowed
|
// Convert ModeAuto to mid-range if dynamic not allowed
|
||||||
if normalized.Mode == ModeAuto && !support.DynamicAllowed {
|
if normalized.Mode == ModeAuto && !support.DynamicAllowed {
|
||||||
normalized = convertAutoToMidRange(normalized, support)
|
normalized = convertAutoToMidRange(normalized, support, provider, model)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch normalized.Mode {
|
switch normalized.Mode {
|
||||||
case ModeBudget, ModeAuto, ModeNone:
|
case ModeBudget, ModeAuto, ModeNone:
|
||||||
clamped := ClampBudgetWithZeroCheck(normalized.Budget, support.Min, support.Max, support.ZeroAllowed)
|
normalized.Budget = ClampBudget(normalized.Budget, modelInfo, provider)
|
||||||
normalized.Budget = clamped
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ModeNone with clamped Budget > 0: set Level to lowest for Level-only/Hybrid models
|
// ModeNone with clamped Budget > 0: set Level to lowest for Level-only/Hybrid models
|
||||||
@@ -213,17 +200,18 @@ func normalizeLevels(levels []string) []string {
|
|||||||
// Logging:
|
// Logging:
|
||||||
// - Debug level when conversion occurs
|
// - Debug level when conversion occurs
|
||||||
// - Fields: original_mode, clamped_to, reason
|
// - 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
|
// 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 {
|
if len(support.Levels) > 0 && support.Min == 0 && support.Max == 0 {
|
||||||
config.Mode = ModeLevel
|
config.Mode = ModeLevel
|
||||||
config.Level = LevelMedium
|
config.Level = LevelMedium
|
||||||
config.Budget = 0
|
config.Budget = 0
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
|
"provider": provider,
|
||||||
|
"model": model,
|
||||||
"original_mode": "auto",
|
"original_mode": "auto",
|
||||||
"clamped_to": string(LevelMedium),
|
"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
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,16 +228,19 @@ func convertAutoToMidRange(config ThinkingConfig, support *registry.ThinkingSupp
|
|||||||
config.Budget = mid
|
config.Budget = mid
|
||||||
}
|
}
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
|
"provider": provider,
|
||||||
|
"model": model,
|
||||||
"original_mode": "auto",
|
"original_mode": "auto",
|
||||||
"clamped_to": config.Budget,
|
"clamped_to": config.Budget,
|
||||||
"reason": "dynamic_not_allowed",
|
}).Debug("thinking: mode converted: dynamic not allowed")
|
||||||
}).Debug("thinking mode converted: dynamic not allowed")
|
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
// logClamp logs a debug message when budget clamping occurs.
|
// 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{
|
log.WithFields(log.Fields{
|
||||||
|
"provider": provider,
|
||||||
|
"model": model,
|
||||||
"original_value": original,
|
"original_value": original,
|
||||||
"min": min,
|
"min": min,
|
||||||
"max": max,
|
"max": max,
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import (
|
|||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
"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/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"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
|
// Check for thinkingBudget first - if present, enable thinking with budget
|
||||||
if thinkingBudget := thinkingConfig.Get("thinkingBudget"); thinkingBudget.Exists() && thinkingBudget.Int() > 0 {
|
if thinkingBudget := thinkingConfig.Get("thinkingBudget"); thinkingBudget.Exists() && thinkingBudget.Int() > 0 {
|
||||||
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
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", thinkingBudget.Int())
|
||||||
out, _ = sjson.Set(out, "thinking.budget_tokens", normalizedBudget)
|
|
||||||
} else if includeThoughts := thinkingConfig.Get("include_thoughts"); includeThoughts.Exists() && includeThoughts.Type == gjson.True {
|
} else if includeThoughts := thinkingConfig.Get("include_thoughts"); includeThoughts.Exists() && includeThoughts.Type == gjson.True {
|
||||||
// Fallback to include_thoughts if no budget specified
|
// Fallback to include_thoughts if no budget specified
|
||||||
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
||||||
|
|||||||
Reference in New Issue
Block a user