mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 13:00:52 +08:00
refactor: improve thinking logic
This commit is contained in:
233
internal/thinking/convert.go
Normal file
233
internal/thinking/convert.go
Normal file
@@ -0,0 +1,233 @@
|
||||
package thinking
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
||||
)
|
||||
|
||||
// levelToBudgetMap defines the standard Level → Budget mapping.
|
||||
// All keys are lowercase; lookups should use strings.ToLower.
|
||||
var levelToBudgetMap = map[string]int{
|
||||
"none": 0,
|
||||
"auto": -1,
|
||||
"minimal": 512,
|
||||
"low": 1024,
|
||||
"medium": 8192,
|
||||
"high": 24576,
|
||||
"xhigh": 32768,
|
||||
}
|
||||
|
||||
// ConvertLevelToBudget converts a thinking level to a budget value.
|
||||
//
|
||||
// This is a semantic conversion that maps discrete levels to numeric budgets.
|
||||
// Level matching is case-insensitive.
|
||||
//
|
||||
// Level → Budget mapping:
|
||||
// - none → 0
|
||||
// - auto → -1
|
||||
// - minimal → 512
|
||||
// - low → 1024
|
||||
// - medium → 8192
|
||||
// - high → 24576
|
||||
// - xhigh → 32768
|
||||
//
|
||||
// Returns:
|
||||
// - budget: The converted budget value
|
||||
// - ok: true if level is valid, false otherwise
|
||||
func ConvertLevelToBudget(level string) (int, bool) {
|
||||
budget, ok := levelToBudgetMap[strings.ToLower(level)]
|
||||
return budget, ok
|
||||
}
|
||||
|
||||
// BudgetThreshold constants define the upper bounds for each thinking level.
|
||||
// These are used by ConvertBudgetToLevel for range-based mapping.
|
||||
const (
|
||||
// ThresholdMinimal is the upper bound for "minimal" level (1-512)
|
||||
ThresholdMinimal = 512
|
||||
// ThresholdLow is the upper bound for "low" level (513-1024)
|
||||
ThresholdLow = 1024
|
||||
// ThresholdMedium is the upper bound for "medium" level (1025-8192)
|
||||
ThresholdMedium = 8192
|
||||
// ThresholdHigh is the upper bound for "high" level (8193-24576)
|
||||
ThresholdHigh = 24576
|
||||
)
|
||||
|
||||
// ConvertBudgetToLevel converts a budget value to the nearest thinking level.
|
||||
//
|
||||
// This is a semantic conversion that maps numeric budgets to discrete levels.
|
||||
// Uses threshold-based mapping for range conversion.
|
||||
//
|
||||
// Budget → Level thresholds:
|
||||
// - -1 → auto
|
||||
// - 0 → none
|
||||
// - 1-512 → minimal
|
||||
// - 513-1024 → low
|
||||
// - 1025-8192 → medium
|
||||
// - 8193-24576 → high
|
||||
// - 24577+ → xhigh
|
||||
//
|
||||
// Returns:
|
||||
// - level: The converted thinking level string
|
||||
// - ok: true if budget is valid, false for invalid negatives (< -1)
|
||||
func ConvertBudgetToLevel(budget int) (string, bool) {
|
||||
switch {
|
||||
case budget < -1:
|
||||
// Invalid negative values
|
||||
return "", false
|
||||
case budget == -1:
|
||||
return string(LevelAuto), true
|
||||
case budget == 0:
|
||||
return string(LevelNone), true
|
||||
case budget <= ThresholdMinimal:
|
||||
return string(LevelMinimal), true
|
||||
case budget <= ThresholdLow:
|
||||
return string(LevelLow), true
|
||||
case budget <= ThresholdMedium:
|
||||
return string(LevelMedium), true
|
||||
case budget <= ThresholdHigh:
|
||||
return string(LevelHigh), true
|
||||
default:
|
||||
return string(LevelXHigh), true
|
||||
}
|
||||
}
|
||||
|
||||
// ModelCapability describes the thinking format support of a model.
|
||||
type ModelCapability int
|
||||
|
||||
const (
|
||||
// CapabilityUnknown indicates modelInfo is nil (passthrough behavior, internal use).
|
||||
CapabilityUnknown ModelCapability = iota - 1
|
||||
// CapabilityNone indicates model doesn't support thinking (Thinking is nil).
|
||||
CapabilityNone
|
||||
// CapabilityBudgetOnly indicates the model supports numeric budgets only.
|
||||
CapabilityBudgetOnly
|
||||
// CapabilityLevelOnly indicates the model supports discrete levels only.
|
||||
CapabilityLevelOnly
|
||||
// CapabilityHybrid indicates the model supports both budgets and levels.
|
||||
CapabilityHybrid
|
||||
)
|
||||
|
||||
// detectModelCapability determines the thinking format capability of a model.
|
||||
//
|
||||
// This is an internal function used by NormalizeForModel to decide conversion strategy.
|
||||
// It analyzes the model's ThinkingSupport configuration to classify the model:
|
||||
// - CapabilityNone: modelInfo.Thinking is nil (model doesn't support thinking)
|
||||
// - CapabilityBudgetOnly: Has Min/Max but no Levels (Claude, Gemini 2.5)
|
||||
// - CapabilityLevelOnly: Has Levels but no Min/Max (OpenAI, iFlow)
|
||||
// - CapabilityHybrid: Has both Min/Max and Levels (Gemini 3)
|
||||
//
|
||||
// Note: Returns a special sentinel value when modelInfo itself is nil (unknown model).
|
||||
func detectModelCapability(modelInfo *registry.ModelInfo) ModelCapability {
|
||||
if modelInfo == nil {
|
||||
return CapabilityUnknown // sentinel for "passthrough" behavior
|
||||
}
|
||||
if modelInfo.Thinking == nil {
|
||||
return CapabilityNone
|
||||
}
|
||||
support := modelInfo.Thinking
|
||||
hasBudget := support.Min > 0 || support.Max > 0
|
||||
hasLevels := len(support.Levels) > 0
|
||||
|
||||
switch {
|
||||
case hasBudget && hasLevels:
|
||||
return CapabilityHybrid
|
||||
case hasBudget:
|
||||
return CapabilityBudgetOnly
|
||||
case hasLevels:
|
||||
return CapabilityLevelOnly
|
||||
default:
|
||||
return CapabilityNone
|
||||
}
|
||||
}
|
||||
|
||||
// normalizeMixedConfig resolves a thinking configuration when both budget and level
|
||||
// might be present, applying priority rules.
|
||||
//
|
||||
// Priority rules (Level takes precedence over Budget):
|
||||
// - If level is non-empty: use level (special handling for "auto" and "none")
|
||||
// - If level is empty and budget is set: use budget
|
||||
// - If neither is set (budget=0, level=""): return ModeNone
|
||||
//
|
||||
// This function is used internally to handle ambiguous input configurations.
|
||||
func normalizeMixedConfig(budget int, level string) ThinkingConfig {
|
||||
normalizedLevel := strings.ToLower(strings.TrimSpace(level))
|
||||
if normalizedLevel != "" {
|
||||
switch normalizedLevel {
|
||||
case string(LevelAuto):
|
||||
return ThinkingConfig{Mode: ModeAuto, Budget: -1, Level: ThinkingLevel(normalizedLevel)}
|
||||
case string(LevelNone):
|
||||
return ThinkingConfig{Mode: ModeNone, Budget: 0, Level: ThinkingLevel(normalizedLevel)}
|
||||
default:
|
||||
return ThinkingConfig{Mode: ModeLevel, Level: ThinkingLevel(normalizedLevel)}
|
||||
}
|
||||
}
|
||||
switch budget {
|
||||
case -1:
|
||||
return ThinkingConfig{Mode: ModeAuto, Budget: -1}
|
||||
case 0:
|
||||
return ThinkingConfig{Mode: ModeNone, Budget: 0}
|
||||
default:
|
||||
return ThinkingConfig{Mode: ModeBudget, Budget: budget}
|
||||
}
|
||||
}
|
||||
|
||||
// NormalizeForModel normalizes a thinking configuration for a specific model.
|
||||
//
|
||||
// This function converts the configuration format based on model capabilities:
|
||||
// - Budget-only models (Claude, Gemini 2.5): Level → Budget conversion
|
||||
// - Level-only models (OpenAI, iFlow): Budget → Level conversion
|
||||
// - Hybrid models (Gemini 3): preserve the original format
|
||||
// - No thinking support (Thinking is nil): degrade to ModeNone
|
||||
// - Unknown model (modelInfo is nil): passthrough (preserve original format)
|
||||
//
|
||||
// Parameters:
|
||||
// - config: The thinking configuration to normalize (must not be nil)
|
||||
// - modelInfo: Model registry information containing ThinkingSupport properties
|
||||
//
|
||||
// Returns:
|
||||
// - Normalized ThinkingConfig suitable for the model
|
||||
// - Error if conversion fails (e.g., unsupported level or invalid budget)
|
||||
func NormalizeForModel(config *ThinkingConfig, modelInfo *registry.ModelInfo) (*ThinkingConfig, error) {
|
||||
if config == nil {
|
||||
return nil, fmt.Errorf("thinking config is nil")
|
||||
}
|
||||
|
||||
normalized := *config
|
||||
capability := detectModelCapability(modelInfo)
|
||||
|
||||
// If model doesn't support thinking, degrade to ModeNone
|
||||
if capability == CapabilityNone && config.Mode != ModeNone && config.Mode != ModeAuto {
|
||||
return &ThinkingConfig{Mode: ModeNone, Budget: 0}, nil
|
||||
}
|
||||
|
||||
switch config.Mode {
|
||||
case ModeAuto, ModeNone:
|
||||
return &normalized, nil
|
||||
case ModeBudget:
|
||||
if capability == CapabilityLevelOnly {
|
||||
level, ok := ConvertBudgetToLevel(config.Budget)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid budget: %d", config.Budget)
|
||||
}
|
||||
normalized.Mode = ModeLevel
|
||||
normalized.Level = ThinkingLevel(level)
|
||||
normalized.Budget = 0
|
||||
}
|
||||
return &normalized, nil
|
||||
case ModeLevel:
|
||||
if capability == CapabilityBudgetOnly {
|
||||
budget, ok := ConvertLevelToBudget(string(config.Level))
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown level: %s", config.Level)
|
||||
}
|
||||
normalized.Mode = ModeBudget
|
||||
normalized.Budget = budget
|
||||
normalized.Level = ""
|
||||
}
|
||||
return &normalized, nil
|
||||
default:
|
||||
return &normalized, nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user