// Package thinking provides unified thinking configuration processing logic.package thinking import ( "fmt" "strings" "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" log "github.com/sirupsen/logrus") // ValidateConfig validates a thinking configuration against model capabilities.//// This function performs comprehensive validation:// - Checks if the model supports thinking// - Auto-converts between Budget and Level formats based on model capability// - Validates that requested level is in the model's supported levels list// - Clamps budget values to model's allowed range// - When converting Budget -> Level for level-only models, clamps the derived standard level to the nearest supported level// (special values none/auto are preserved)// - When config comes from a model suffix, strict budget validation is disabled (we clamp instead of error)//// Parameters:// - config: The thinking configuration to validate// - support: Model's ThinkingSupport properties (nil means no thinking support)// - fromFormat: Source provider format (used to determine strict validation rules)// - toFormat: Target provider format// - fromSuffix: Whether config was sourced from model suffix//// Returns:// - Normalized ThinkingConfig with clamped values// - ThinkingError if validation fails (ErrThinkingNotSupported, ErrLevelNotSupported, etc.)//// Auto-conversion behavior:// - Budget-only model + Level config → Level converted to Budget// - Level-only model + Budget config → Budget converted to Level// - Hybrid model → preserve original formatfunc ValidateConfig(config ThinkingConfig, modelInfo *registry.ModelInfo, fromFormat, toFormat string, fromSuffix bool) (*ThinkingConfig, error) { fromFormat, toFormat = strings.ToLower(strings.TrimSpace(fromFormat)), strings.ToLower(strings.TrimSpace(toFormat)) 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", model) } return &config, nil } // allowClampUnsupported determines whether to clamp unsupported levels instead of returning an error. // This applies when crossing provider families (e.g., openai→gemini, claude→gemini) and the target // model supports discrete levels. Same-family conversions require strict validation. // // modelFamilyMismatch covers providers that reuse another protocol on the wire // (e.g. Kimi serving Claude-compatible /v1/messages). In that path fromFormat and // toFormat both look like "claude", but the model itself is not Claude-family, so // unsupported levels such as "max" should clamp to the nearest supported level // (typically "high") instead of failing validation. toCapability := detectModelCapability(modelInfo) toHasLevelSupport := toCapability == CapabilityLevelOnly || toCapability == CapabilityHybrid modelFamilyMismatch := false if modelInfo != nil { modelType := strings.ToLower(strings.TrimSpace(modelInfo.Type)) if modelType != "" { if (fromFormat != "" && !isSameProviderFamily(fromFormat, modelType)) || (toFormat != "" && !isSameProviderFamily(toFormat, modelType)) { modelFamilyMismatch = true } } } allowClampUnsupported := toHasLevelSupport && (!isSameProviderFamily(fromFormat, toFormat) || modelFamilyMismatch) // strictBudget determines whether to enforce strict budget range validation. // This applies when: (1) config comes from request body (not suffix), (2) source format is known, // and (3) source and target are in the same provider family. Cross-family or suffix-based configs // are clamped instead of rejected to improve interoperability. strictBudget := !fromSuffix && fromFormat != "" && isSameProviderFamily(fromFormat, toFormat) && !modelFamilyMismatch budgetDerivedFromLevel := false capability := detectModelCapability(modelInfo) switch capability { case CapabilityBudgetOnly: if config.Mode == ModeLevel { if config.Level == LevelAuto { break } budget, ok := ConvertLevelToBudget(string(config.Level)) if !ok { return nil, NewThinkingError(ErrUnknownLevel, fmt.Sprintf("unknown level: %s", config.Level)) } config.Mode = ModeBudget config.Budget = budget config.Level = "" budgetDerivedFromLevel = true } case CapabilityLevelOnly: if config.Mode == ModeBudget { level, ok := ConvertBudgetToLevel(config.Budget) if !ok { return nil, NewThinkingError(ErrUnknownLevel, fmt.Sprintf("budget %d cannot be converted to a valid level", config.Budget)) } // When converting Budget -> Level for level-only models, clamp the derived standard level // to the nearest supported level. Special values (none/auto) are preserved. config.Mode = ModeLevel config.Level = clampLevel(ThinkingLevel(level), modelInfo, toFormat) config.Budget = 0 } case CapabilityHybrid: } if config.Mode == ModeLevel && config.Level == LevelNone { config.Mode = ModeNone config.Budget = 0 config.Level = "" } if config.Mode == ModeLevel && config.Level == LevelAuto { config.Mode = ModeAuto config.Budget = -1 config.Level = "" } if config.Mode == ModeBudget && config.Budget == 0 { config.Mode = ModeNone config.Level = "" } if len(support.Levels) > 0 && config.Mode == ModeLevel { if !isLevelSupported(string(config.Level), support.Levels) { if allowClampUnsupported { config.Level = clampLevel(config.Level, modelInfo, toFormat) } if !isLevelSupported(string(config.Level), support.Levels) { // User explicitly specified an unsupported level - return error // (budget-derived levels may be clamped based on source format) validLevels := normalizeLevels(support.Levels) message := fmt.Sprintf("level %q not supported, valid levels: %s", strings.ToLower(string(config.Level)), strings.Join(validLevels, ", ")) return nil, NewThinkingError(ErrLevelNotSupported, message) } } } if strictBudget && config.Mode == ModeBudget && !budgetDerivedFromLevel { min, max := support.Min, support.Max if min != 0 || max != 0 { if config.Budget < min || config.Budget > max || (config.Budget == 0 && !support.ZeroAllowed) { message := fmt.Sprintf("budget %d out of range [%d,%d]", config.Budget, min, max) return nil, NewThinkingError(ErrBudgetOutOfRange, message) } } } // Convert ModeAuto to mid-range if dynamic not allowed if config.Mode == ModeAuto && !support.DynamicAllowed { config = convertAutoToMidRange(config, support, toFormat, model) // The canonical mid-range level may not be present in a model's discrete // level subset (for example, Levels=[low, high]). Clamp the generated // fallback just like a budget-derived level so providers never receive an // unsupported value. if config.Mode == ModeLevel && len(support.Levels) > 0 && !isLevelSupported(string(config.Level), support.Levels) { config.Level = clampLevel(config.Level, modelInfo, toFormat) } } if config.Mode == ModeNone && toFormat == "claude" { // Claude supports explicit disable via thinking.type="disabled". // Keep Budget=0 so applier can omit budget_tokens. config.Budget = 0 config.Level = "" } else { switch config.Mode { case ModeBudget, ModeAuto, ModeNone: config.Budget = clampBudget(config.Budget, modelInfo, toFormat) } // ModeNone for a model that cannot be disabled falls back to the lowest // supported level. Budget-capable models reach this path with Budget > 0; // level-only models need the capability flags checked explicitly because // their Min/Max range is zero. cannotDisableLevelModel := !support.ZeroAllowed && !isLevelSupported(string(LevelNone), support.Levels) if config.Mode == ModeNone && len(support.Levels) > 0 && (config.Budget > 0 || cannotDisableLevelModel) { config.Level = ThinkingLevel(support.Levels[0]) } } return &config, nil} // convertAutoToMidRange converts ModeAuto to a mid-range value when dynamic is not allowed.//// This function handles the case where a model does not support dynamic/auto thinking.// The auto mode is silently converted to a fixed value based on model capability:// - Level-only models: convert to ModeLevel with LevelMedium// - Budget models: convert to ModeBudget with mid = (Min + Max) / 2//// Logging:// - Debug level when conversion occurs// - Fields: original_mode, clamped_to, reasonfunc 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), }).Debug("thinking: mode converted, dynamic not allowed, using medium level |") return config } // For budget models, use mid-range budget mid := (support.Min + support.Max) / 2 if mid <= 0 && support.ZeroAllowed { config.Mode = ModeNone config.Budget = 0 } else if mid <= 0 { config.Mode = ModeBudget config.Budget = support.Min } else { config.Mode = ModeBudget config.Budget = mid } log.WithFields(log.Fields{ "provider": provider, "model": model, "original_mode": "auto", "clamped_to": config.Budget, }).Debug("thinking: mode converted, dynamic not allowed |") return config} // standardLevelOrder defines the canonical ordering of thinking levels from lowest to highest.var standardLevelOrder = []ThinkingLevel{LevelMinimal, LevelLow, LevelMedium, LevelHigh, LevelXHigh, LevelMax} // clampLevel clamps the given level to the nearest supported level.// On tie, prefers the lower level.func clampLevel(level ThinkingLevel, modelInfo *registry.ModelInfo, provider string) ThinkingLevel { model := "unknown" var supported []string if modelInfo != nil { if modelInfo.ID != "" { model = modelInfo.ID } if modelInfo.Thinking != nil { supported = modelInfo.Thinking.Levels } } if len(supported) == 0 || isLevelSupported(string(level), supported) { return level } pos := levelIndex(string(level)) if pos == -1 { return level } bestIdx, bestDist := -1, len(standardLevelOrder)+1 for _, s := range supported { if idx := levelIndex(strings.TrimSpace(s)); idx != -1 { if dist := abs(pos - idx); dist < bestDist || (dist == bestDist && idx < bestIdx) { bestIdx, bestDist = idx, dist } } } if bestIdx >= 0 { clamped := standardLevelOrder[bestIdx] log.WithFields(log.Fields{ "provider": provider, "model": model, "original_value": string(level), "clamped_to": string(clamped), }).Debug("thinking: level clamped |") return clamped } return level} // clampBudget clamps a budget value to the model's supported range.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 } min, max := support.Min, support.Max if value == 0 && !support.ZeroAllowed { log.WithFields(log.Fields{ "provider": provider, "model": model, "original_value": value, "clamped_to": min, "min": min, "max": max, }).Warn("thinking: budget zero not allowed |") return min } // Some models are level-only and do not define numeric budget ranges. if min == 0 && max == 0 { return value } if value < min { if value == 0 && support.ZeroAllowed { return 0 } logClamp(provider, model, value, min, min, max) return min } if value > max { logClamp(provider, model, value, max, min, max) return max } return value} func isLevelSupported(level string, supported []string) bool { for _, s := range supported { if strings.EqualFold(level, strings.TrimSpace(s)) { return true } } return false} func levelIndex(level string) int { for i, l := range standardLevelOrder { if strings.EqualFold(level, string(l)) { return i } } return -1} func normalizeLevels(levels []string) []string { out := make([]string, len(levels)) for i, l := range levels { out[i] = strings.ToLower(strings.TrimSpace(l)) } return out} // isBudgetCapableProvider returns true if the provider supports budget-based thinking.// These providers may also support level-based thinking (hybrid models).func isBudgetCapableProvider(provider string) bool { switch provider { case "gemini", "antigravity", "claude": return true default: return false }} func isGeminiFamily(provider string) bool { switch provider { case "gemini", "antigravity": return true default: return false }} func isOpenAIFamily(provider string) bool { switch provider { case "openai", "openai-response", "codex": return true default: return false }} func isSameProviderFamily(from, to string) bool { if from == to { return true } return (isGeminiFamily(from) && isGeminiFamily(to)) || (isOpenAIFamily(from) && isOpenAIFamily(to))} func abs(x int) int { if x < 0 { return -x } return x} 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, "clamped_to": clampedTo, }).Debug("thinking: budget clamped |")}
// Package thinking provides unified thinking configuration processing logic.
package thinking
import (
"fmt"
"strings"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
log "github.com/sirupsen/logrus"
)
// ValidateConfig validates a thinking configuration against model capabilities.
//
// This function performs comprehensive validation:
// - Checks if the model supports thinking
// - Auto-converts between Budget and Level formats based on model capability
// - Validates that requested level is in the model's supported levels list
// - Clamps budget values to model's allowed range
// - When converting Budget -> Level for level-only models, clamps the derived standard level to the nearest supported level
// (special values none/auto are preserved)
// - When config comes from a model suffix, strict budget validation is disabled (we clamp instead of error)
//
// Parameters:
// - config: The thinking configuration to validate
// - support: Model's ThinkingSupport properties (nil means no thinking support)
// - fromFormat: Source provider format (used to determine strict validation rules)
// - toFormat: Target provider format
// - fromSuffix: Whether config was sourced from model suffix
//
// Returns:
// - Normalized ThinkingConfig with clamped values
// - ThinkingError if validation fails (ErrThinkingNotSupported, ErrLevelNotSupported, etc.)
//
// Auto-conversion behavior:
// - 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, modelInfo *registry.ModelInfo, fromFormat, toFormat string, fromSuffix bool) (*ThinkingConfig, error) {
fromFormat, toFormat = strings.ToLower(strings.TrimSpace(fromFormat)), strings.ToLower(strings.TrimSpace(toFormat))
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", model)
}
return &config, nil
}
// allowClampUnsupported determines whether to clamp unsupported levels instead of returning an error.
// This applies when crossing provider families (e.g., openai→gemini, claude→gemini) and the target
// model supports discrete levels. Same-family conversions require strict validation.
//
// modelFamilyMismatch covers providers that reuse another protocol on the wire
// (e.g. Kimi serving Claude-compatible /v1/messages). In that path fromFormat and
// toFormat both look like "claude", but the model itself is not Claude-family, so
// unsupported levels such as "max" should clamp to the nearest supported level
// (typically "high") instead of failing validation.
toCapability := detectModelCapability(modelInfo)
toHasLevelSupport := toCapability == CapabilityLevelOnly || toCapability == CapabilityHybrid
modelFamilyMismatch := false
if modelInfo != nil {
modelType := strings.ToLower(strings.TrimSpace(modelInfo.Type))
if modelType != "" {
if (fromFormat != "" && !isSameProviderFamily(fromFormat, modelType)) ||
(toFormat != "" && !isSameProviderFamily(toFormat, modelType)) {
modelFamilyMismatch = true
}
}
}
allowClampUnsupported := toHasLevelSupport && (!isSameProviderFamily(fromFormat, toFormat) || modelFamilyMismatch)
// strictBudget determines whether to enforce strict budget range validation.
// This applies when: (1) config comes from request body (not suffix), (2) source format is known,
// and (3) source and target are in the same provider family. Cross-family or suffix-based configs
// are clamped instead of rejected to improve interoperability.
strictBudget := !fromSuffix && fromFormat != "" && isSameProviderFamily(fromFormat, toFormat) && !modelFamilyMismatch
budgetDerivedFromLevel := false
capability := detectModelCapability(modelInfo)
switch capability {
case CapabilityBudgetOnly:
if config.Mode == ModeLevel {
if config.Level == LevelAuto {
break
}
budget, ok := ConvertLevelToBudget(string(config.Level))
if !ok {
return nil, NewThinkingError(ErrUnknownLevel, fmt.Sprintf("unknown level: %s", config.Level))
}
config.Mode = ModeBudget
config.Budget = budget
config.Level = ""
budgetDerivedFromLevel = true
}
case CapabilityLevelOnly:
if config.Mode == ModeBudget {
level, ok := ConvertBudgetToLevel(config.Budget)
if !ok {
return nil, NewThinkingError(ErrUnknownLevel, fmt.Sprintf("budget %d cannot be converted to a valid level", config.Budget))
}
// When converting Budget -> Level for level-only models, clamp the derived standard level
// to the nearest supported level. Special values (none/auto) are preserved.
config.Mode = ModeLevel
config.Level = clampLevel(ThinkingLevel(level), modelInfo, toFormat)
config.Budget = 0
}
case CapabilityHybrid:
}
if config.Mode == ModeLevel && config.Level == LevelNone {
config.Mode = ModeNone
config.Budget = 0
config.Level = ""
}
if config.Mode == ModeLevel && config.Level == LevelAuto {
config.Mode = ModeAuto
config.Budget = -1
config.Level = ""
}
if config.Mode == ModeBudget && config.Budget == 0 {
config.Mode = ModeNone
config.Level = ""
}
if len(support.Levels) > 0 && config.Mode == ModeLevel {
if !isLevelSupported(string(config.Level), support.Levels) {
if allowClampUnsupported {
config.Level = clampLevel(config.Level, modelInfo, toFormat)
}
if !isLevelSupported(string(config.Level), support.Levels) {
// User explicitly specified an unsupported level - return error
// (budget-derived levels may be clamped based on source format)
validLevels := normalizeLevels(support.Levels)
message := fmt.Sprintf("level %q not supported, valid levels: %s", strings.ToLower(string(config.Level)), strings.Join(validLevels, ", "))
return nil, NewThinkingError(ErrLevelNotSupported, message)
}
}
}
if strictBudget && config.Mode == ModeBudget && !budgetDerivedFromLevel {
min, max := support.Min, support.Max
if min != 0 || max != 0 {
if config.Budget < min || config.Budget > max || (config.Budget == 0 && !support.ZeroAllowed) {
message := fmt.Sprintf("budget %d out of range [%d,%d]", config.Budget, min, max)
return nil, NewThinkingError(ErrBudgetOutOfRange, message)
}
}
}
// Convert ModeAuto to mid-range if dynamic not allowed
if config.Mode == ModeAuto && !support.DynamicAllowed {
config = convertAutoToMidRange(config, support, toFormat, model)
// The canonical mid-range level may not be present in a model's discrete
// level subset (for example, Levels=[low, high]). Clamp the generated
// fallback just like a budget-derived level so providers never receive an
// unsupported value.
if config.Mode == ModeLevel && len(support.Levels) > 0 && !isLevelSupported(string(config.Level), support.Levels) {
config.Level = clampLevel(config.Level, modelInfo, toFormat)
}
}
if config.Mode == ModeNone && toFormat == "claude" {
// Claude supports explicit disable via thinking.type="disabled".
// Keep Budget=0 so applier can omit budget_tokens.
config.Budget = 0
config.Level = ""
} else {
switch config.Mode {
case ModeBudget, ModeAuto, ModeNone:
config.Budget = clampBudget(config.Budget, modelInfo, toFormat)
}
// ModeNone for a model that cannot be disabled falls back to the lowest
// supported level. Budget-capable models reach this path with Budget > 0;
// level-only models need the capability flags checked explicitly because
// their Min/Max range is zero.
cannotDisableLevelModel := !support.ZeroAllowed && !isLevelSupported(string(LevelNone), support.Levels)
if config.Mode == ModeNone && len(support.Levels) > 0 && (config.Budget > 0 || cannotDisableLevelModel) {
config.Level = ThinkingLevel(support.Levels[0])
}
}
return &config, nil
}
// convertAutoToMidRange converts ModeAuto to a mid-range value when dynamic is not allowed.
//
// This function handles the case where a model does not support dynamic/auto thinking.
// The auto mode is silently converted to a fixed value based on model capability:
// - Level-only models: convert to ModeLevel with LevelMedium
// - Budget models: convert to ModeBudget with mid = (Min + Max) / 2
//
// Logging:
// - Debug level when conversion occurs
// - Fields: original_mode, clamped_to, reason
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),
}).Debug("thinking: mode converted, dynamic not allowed, using medium level |")
return config
}
// For budget models, use mid-range budget
mid := (support.Min + support.Max) / 2
if mid <= 0 && support.ZeroAllowed {
config.Mode = ModeNone
config.Budget = 0
} else if mid <= 0 {
config.Mode = ModeBudget
config.Budget = support.Min
} else {
config.Mode = ModeBudget
config.Budget = mid
}
log.WithFields(log.Fields{
"provider": provider,
"model": model,
"original_mode": "auto",
"clamped_to": config.Budget,
}).Debug("thinking: mode converted, dynamic not allowed |")
return config
}
// standardLevelOrder defines the canonical ordering of thinking levels from lowest to highest.
var standardLevelOrder = []ThinkingLevel{LevelMinimal, LevelLow, LevelMedium, LevelHigh, LevelXHigh, LevelMax}
// clampLevel clamps the given level to the nearest supported level.
// On tie, prefers the lower level.
func clampLevel(level ThinkingLevel, modelInfo *registry.ModelInfo, provider string) ThinkingLevel {
model := "unknown"
var supported []string
if modelInfo != nil {
if modelInfo.ID != "" {
model = modelInfo.ID
}
if modelInfo.Thinking != nil {
supported = modelInfo.Thinking.Levels
}
}
if len(supported) == 0 || isLevelSupported(string(level), supported) {
return level
}
pos := levelIndex(string(level))
if pos == -1 {
return level
}
bestIdx, bestDist := -1, len(standardLevelOrder)+1
for _, s := range supported {
if idx := levelIndex(strings.TrimSpace(s)); idx != -1 {
if dist := abs(pos - idx); dist < bestDist || (dist == bestDist && idx < bestIdx) {
bestIdx, bestDist = idx, dist
}
}
}
if bestIdx >= 0 {
clamped := standardLevelOrder[bestIdx]
log.WithFields(log.Fields{
"provider": provider,
"model": model,
"original_value": string(level),
"clamped_to": string(clamped),
}).Debug("thinking: level clamped |")
return clamped
}
return level
}
// clampBudget clamps a budget value to the model's supported range.
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
}
min, max := support.Min, support.Max
if value == 0 && !support.ZeroAllowed {
log.WithFields(log.Fields{
"provider": provider,
"model": model,
"original_value": value,
"clamped_to": min,
"min": min,
"max": max,
}).Warn("thinking: budget zero not allowed |")
return min
}
// Some models are level-only and do not define numeric budget ranges.
if min == 0 && max == 0 {
return value
}
if value < min {
if value == 0 && support.ZeroAllowed {
return 0
}
logClamp(provider, model, value, min, min, max)
return min
}
if value > max {
logClamp(provider, model, value, max, min, max)
return max
}
return value
}
func isLevelSupported(level string, supported []string) bool {
for _, s := range supported {
if strings.EqualFold(level, strings.TrimSpace(s)) {
return true
}
}
return false
}
func levelIndex(level string) int {
for i, l := range standardLevelOrder {
if strings.EqualFold(level, string(l)) {
return i
}
}
return -1
}
func normalizeLevels(levels []string) []string {
out := make([]string, len(levels))
for i, l := range levels {
out[i] = strings.ToLower(strings.TrimSpace(l))
}
return out
}
// isBudgetCapableProvider returns true if the provider supports budget-based thinking.
// These providers may also support level-based thinking (hybrid models).
func isBudgetCapableProvider(provider string) bool {
switch provider {
case "gemini", "antigravity", "claude":
return true
default:
return false
}
}
func isGeminiFamily(provider string) bool {
switch provider {
case "gemini", "antigravity":
return true
default:
return false
}
}
func isOpenAIFamily(provider string) bool {
switch provider {
case "openai", "openai-response", "codex":
return true
default:
return false
}
}
func isSameProviderFamily(from, to string) bool {
if from == to {
return true
}
return (isGeminiFamily(from) && isGeminiFamily(to)) ||
(isOpenAIFamily(from) && isOpenAIFamily(to))
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
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,
"clamped_to": clampedTo,
}).Debug("thinking: budget clamped |")
}