| |
| package thinking |
|
|
| import ( |
| "strings" |
|
|
| "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" |
| log "github.com/sirupsen/logrus" |
| "github.com/tidwall/gjson" |
| ) |
|
|
| |
| var providerAppliers = map[string]ProviderApplier{ |
| "gemini": nil, |
| "gemini-cli": nil, |
| "claude": nil, |
| "openai": nil, |
| "codex": nil, |
| "iflow": nil, |
| "antigravity": nil, |
| } |
|
|
| |
| |
| func GetProviderApplier(provider string) ProviderApplier { |
| return providerAppliers[provider] |
| } |
|
|
| |
| func RegisterProvider(name string, applier ProviderApplier) { |
| providerAppliers[name] = applier |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| func IsUserDefinedModel(modelInfo *registry.ModelInfo) bool { |
| if modelInfo == nil { |
| return true |
| } |
| return modelInfo.UserDefined |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| func ApplyThinking(body []byte, model string, fromFormat string, toFormat string, providerKey string) ([]byte, error) { |
| providerFormat := strings.ToLower(strings.TrimSpace(toFormat)) |
| providerKey = strings.ToLower(strings.TrimSpace(providerKey)) |
| if providerKey == "" { |
| providerKey = providerFormat |
| } |
| fromFormat = strings.ToLower(strings.TrimSpace(fromFormat)) |
| if fromFormat == "" { |
| fromFormat = providerFormat |
| } |
| |
| applier := GetProviderApplier(providerFormat) |
| if applier == nil { |
| log.WithFields(log.Fields{ |
| "provider": providerFormat, |
| "model": model, |
| }).Debug("thinking: unknown provider, passthrough |") |
| return body, nil |
| } |
|
|
| |
| suffixResult := ParseSuffix(model) |
| baseModel := suffixResult.ModelName |
| |
| modelInfo := registry.LookupModelInfo(baseModel, providerKey) |
|
|
| |
| |
| |
| if IsUserDefinedModel(modelInfo) { |
| return applyUserDefinedModel(body, modelInfo, fromFormat, providerFormat, suffixResult) |
| } |
| if modelInfo.Thinking == nil { |
| config := extractThinkingConfig(body, providerFormat) |
| if hasThinkingConfig(config) { |
| log.WithFields(log.Fields{ |
| "model": baseModel, |
| "provider": providerFormat, |
| }).Debug("thinking: model does not support thinking, stripping config |") |
| return StripThinkingConfig(body, providerFormat), nil |
| } |
| log.WithFields(log.Fields{ |
| "provider": providerFormat, |
| "model": baseModel, |
| }).Debug("thinking: model does not support thinking, passthrough |") |
| return body, nil |
| } |
|
|
| |
| var config ThinkingConfig |
| if suffixResult.HasSuffix { |
| config = parseSuffixToConfig(suffixResult.RawSuffix, providerFormat, model) |
| log.WithFields(log.Fields{ |
| "provider": providerFormat, |
| "model": model, |
| "mode": config.Mode, |
| "budget": config.Budget, |
| "level": config.Level, |
| }).Debug("thinking: config from model suffix |") |
| } else { |
| config = extractThinkingConfig(body, providerFormat) |
| if hasThinkingConfig(config) { |
| log.WithFields(log.Fields{ |
| "provider": providerFormat, |
| "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": providerFormat, |
| "model": modelInfo.ID, |
| }).Debug("thinking: no config found, passthrough |") |
| return body, nil |
| } |
|
|
| |
| validated, err := ValidateConfig(config, modelInfo, fromFormat, providerFormat, suffixResult.HasSuffix) |
| if err != nil { |
| log.WithFields(log.Fields{ |
| "provider": providerFormat, |
| "model": modelInfo.ID, |
| "error": err.Error(), |
| }).Warn("thinking: validation failed |") |
| |
| |
| |
| return body, err |
| } |
|
|
| |
| if validated == nil { |
| log.WithFields(log.Fields{ |
| "provider": providerFormat, |
| "model": modelInfo.ID, |
| }).Warn("thinking: ValidateConfig returned nil config without error, passthrough |") |
| return body, nil |
| } |
|
|
| log.WithFields(log.Fields{ |
| "provider": providerFormat, |
| "model": modelInfo.ID, |
| "mode": validated.Mode, |
| "budget": validated.Budget, |
| "level": validated.Level, |
| }).Debug("thinking: processed config to apply |") |
|
|
| |
| return applier.Apply(body, *validated, modelInfo) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| func parseSuffixToConfig(rawSuffix, provider, model string) ThinkingConfig { |
| |
| if mode, ok := ParseSpecialSuffix(rawSuffix); ok { |
| switch mode { |
| case ModeNone: |
| return ThinkingConfig{Mode: ModeNone, Budget: 0} |
| case ModeAuto: |
| return ThinkingConfig{Mode: ModeAuto, Budget: -1} |
| } |
| } |
|
|
| |
| if level, ok := ParseLevelSuffix(rawSuffix); ok { |
| return ThinkingConfig{Mode: ModeLevel, Level: level} |
| } |
|
|
| |
| if budget, ok := ParseNumericSuffix(rawSuffix); ok { |
| if budget == 0 { |
| return ThinkingConfig{Mode: ModeNone, Budget: 0} |
| } |
| return ThinkingConfig{Mode: ModeBudget, Budget: budget} |
| } |
|
|
| |
| log.WithFields(log.Fields{ |
| "provider": provider, |
| "model": model, |
| "raw_suffix": rawSuffix, |
| }).Debug("thinking: unknown suffix format, treating as no config |") |
| return ThinkingConfig{} |
| } |
|
|
| |
| |
| func applyUserDefinedModel(body []byte, modelInfo *registry.ModelInfo, fromFormat, toFormat string, suffixResult SuffixResult) ([]byte, error) { |
| |
| modelID := "" |
| if modelInfo != nil { |
| modelID = modelInfo.ID |
| } else { |
| modelID = suffixResult.ModelName |
| } |
|
|
| |
| var config ThinkingConfig |
| if suffixResult.HasSuffix { |
| config = parseSuffixToConfig(suffixResult.RawSuffix, toFormat, modelID) |
| } else { |
| config = extractThinkingConfig(body, toFormat) |
| } |
|
|
| if !hasThinkingConfig(config) { |
| log.WithFields(log.Fields{ |
| "model": modelID, |
| "provider": toFormat, |
| }).Debug("thinking: user-defined model, passthrough (no config) |") |
| return body, nil |
| } |
|
|
| applier := GetProviderApplier(toFormat) |
| if applier == nil { |
| log.WithFields(log.Fields{ |
| "model": modelID, |
| "provider": toFormat, |
| }).Debug("thinking: user-defined model, passthrough (unknown provider) |") |
| return body, nil |
| } |
|
|
| log.WithFields(log.Fields{ |
| "provider": toFormat, |
| "model": modelID, |
| "mode": config.Mode, |
| "budget": config.Budget, |
| "level": config.Level, |
| }).Debug("thinking: applying config for user-defined model (skip validation)") |
|
|
| config = normalizeUserDefinedConfig(config, fromFormat, toFormat) |
| return applier.Apply(body, config, modelInfo) |
| } |
|
|
| func normalizeUserDefinedConfig(config ThinkingConfig, fromFormat, toFormat string) ThinkingConfig { |
| if config.Mode != ModeLevel { |
| return config |
| } |
| if !isBudgetBasedProvider(toFormat) || !isLevelBasedProvider(fromFormat) { |
| return config |
| } |
| budget, ok := ConvertLevelToBudget(string(config.Level)) |
| if !ok { |
| return config |
| } |
| config.Mode = ModeBudget |
| config.Budget = budget |
| config.Level = "" |
| return config |
| } |
|
|
| |
| func extractThinkingConfig(body []byte, provider string) ThinkingConfig { |
| if len(body) == 0 || !gjson.ValidBytes(body) { |
| return ThinkingConfig{} |
| } |
|
|
| switch provider { |
| case "claude": |
| return extractClaudeConfig(body) |
| case "gemini", "gemini-cli", "antigravity": |
| return extractGeminiConfig(body, provider) |
| case "openai": |
| return extractOpenAIConfig(body) |
| case "codex": |
| return extractCodexConfig(body) |
| case "iflow": |
| config := extractIFlowConfig(body) |
| if hasThinkingConfig(config) { |
| return config |
| } |
| return extractOpenAIConfig(body) |
| default: |
| return ThinkingConfig{} |
| } |
| } |
|
|
| func hasThinkingConfig(config ThinkingConfig) bool { |
| return config.Mode != ModeBudget || config.Budget != 0 || config.Level != "" |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| func extractClaudeConfig(body []byte) ThinkingConfig { |
| thinkingType := gjson.GetBytes(body, "thinking.type").String() |
| if thinkingType == "disabled" { |
| return ThinkingConfig{Mode: ModeNone, Budget: 0} |
| } |
|
|
| |
| if budget := gjson.GetBytes(body, "thinking.budget_tokens"); budget.Exists() { |
| value := int(budget.Int()) |
| switch value { |
| case 0: |
| return ThinkingConfig{Mode: ModeNone, Budget: 0} |
| case -1: |
| return ThinkingConfig{Mode: ModeAuto, Budget: -1} |
| default: |
| return ThinkingConfig{Mode: ModeBudget, Budget: value} |
| } |
| } |
|
|
| |
| if thinkingType == "enabled" { |
| return ThinkingConfig{Mode: ModeAuto, Budget: -1} |
| } |
|
|
| return ThinkingConfig{} |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| func extractGeminiConfig(body []byte, provider string) ThinkingConfig { |
| prefix := "generationConfig.thinkingConfig" |
| if provider == "gemini-cli" || provider == "antigravity" { |
| prefix = "request.generationConfig.thinkingConfig" |
| } |
|
|
| |
| if level := gjson.GetBytes(body, prefix+".thinkingLevel"); level.Exists() { |
| value := level.String() |
| switch value { |
| case "none": |
| return ThinkingConfig{Mode: ModeNone, Budget: 0} |
| case "auto": |
| return ThinkingConfig{Mode: ModeAuto, Budget: -1} |
| default: |
| return ThinkingConfig{Mode: ModeLevel, Level: ThinkingLevel(value)} |
| } |
| } |
|
|
| |
| if budget := gjson.GetBytes(body, prefix+".thinkingBudget"); budget.Exists() { |
| value := int(budget.Int()) |
| switch value { |
| case 0: |
| return ThinkingConfig{Mode: ModeNone, Budget: 0} |
| case -1: |
| return ThinkingConfig{Mode: ModeAuto, Budget: -1} |
| default: |
| return ThinkingConfig{Mode: ModeBudget, Budget: value} |
| } |
| } |
|
|
| return ThinkingConfig{} |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| func extractOpenAIConfig(body []byte) ThinkingConfig { |
| |
| if effort := gjson.GetBytes(body, "reasoning_effort"); effort.Exists() { |
| value := effort.String() |
| if value == "none" { |
| return ThinkingConfig{Mode: ModeNone, Budget: 0} |
| } |
| return ThinkingConfig{Mode: ModeLevel, Level: ThinkingLevel(value)} |
| } |
|
|
| return ThinkingConfig{} |
| } |
|
|
| |
| |
| |
| |
| |
| |
| func extractCodexConfig(body []byte) ThinkingConfig { |
| |
| if effort := gjson.GetBytes(body, "reasoning.effort"); effort.Exists() { |
| value := effort.String() |
| if value == "none" { |
| return ThinkingConfig{Mode: ModeNone, Budget: 0} |
| } |
| return ThinkingConfig{Mode: ModeLevel, Level: ThinkingLevel(value)} |
| } |
|
|
| return ThinkingConfig{} |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| func extractIFlowConfig(body []byte) ThinkingConfig { |
| |
| if enabled := gjson.GetBytes(body, "chat_template_kwargs.enable_thinking"); enabled.Exists() { |
| if enabled.Bool() { |
| |
| return ThinkingConfig{Mode: ModeBudget, Budget: 1} |
| } |
| return ThinkingConfig{Mode: ModeNone, Budget: 0} |
| } |
|
|
| |
| if split := gjson.GetBytes(body, "reasoning_split"); split.Exists() { |
| if split.Bool() { |
| |
| return ThinkingConfig{Mode: ModeBudget, Budget: 1} |
| } |
| return ThinkingConfig{Mode: ModeNone, Budget: 0} |
| } |
|
|
| return ThinkingConfig{} |
| } |
|
|