mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 04:50:52 +08:00
278 lines
9.4 KiB
Go
278 lines
9.4 KiB
Go
// Package thinking provides unified thinking configuration processing logic.
|
|
package thinking
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
|
)
|
|
|
|
// TestConvertLevelToBudget tests the ConvertLevelToBudget function.
|
|
//
|
|
// ConvertLevelToBudget converts a thinking level to a budget value.
|
|
// This is a semantic conversion - it does NOT apply clamping.
|
|
//
|
|
// Level → Budget mapping:
|
|
// - none → 0
|
|
// - auto → -1
|
|
// - minimal → 512
|
|
// - low → 1024
|
|
// - medium → 8192
|
|
// - high → 24576
|
|
// - xhigh → 32768
|
|
func TestConvertLevelToBudget(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
level string
|
|
want int
|
|
wantOK bool
|
|
}{
|
|
// Standard levels
|
|
{"none", "none", 0, true},
|
|
{"auto", "auto", -1, true},
|
|
{"minimal", "minimal", 512, true},
|
|
{"low", "low", 1024, true},
|
|
{"medium", "medium", 8192, true},
|
|
{"high", "high", 24576, true},
|
|
{"xhigh", "xhigh", 32768, true},
|
|
|
|
// Case insensitive
|
|
{"case insensitive HIGH", "HIGH", 24576, true},
|
|
{"case insensitive High", "High", 24576, true},
|
|
{"case insensitive NONE", "NONE", 0, true},
|
|
{"case insensitive Auto", "Auto", -1, true},
|
|
|
|
// Invalid levels
|
|
{"invalid ultra", "ultra", 0, false},
|
|
{"invalid maximum", "maximum", 0, false},
|
|
{"empty string", "", 0, false},
|
|
{"whitespace", " ", 0, false},
|
|
{"numeric string", "1000", 0, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
budget, ok := ConvertLevelToBudget(tt.level)
|
|
if ok != tt.wantOK {
|
|
t.Errorf("ConvertLevelToBudget(%q) ok = %v, want %v", tt.level, ok, tt.wantOK)
|
|
}
|
|
if budget != tt.want {
|
|
t.Errorf("ConvertLevelToBudget(%q) = %d, want %d", tt.level, budget, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestConvertBudgetToLevel tests the ConvertBudgetToLevel function.
|
|
//
|
|
// ConvertBudgetToLevel converts a budget value to the nearest level.
|
|
// 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
|
|
//
|
|
// Depends on: Epic 4 Story 4-2 (budget to level conversion)
|
|
func TestConvertBudgetToLevel(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
budget int
|
|
want string
|
|
wantOK bool
|
|
}{
|
|
// Special values
|
|
{"auto", -1, "auto", true},
|
|
{"none", 0, "none", true},
|
|
|
|
// Invalid negative values
|
|
{"invalid negative -2", -2, "", false},
|
|
{"invalid negative -100", -100, "", false},
|
|
{"invalid negative extreme", -999999, "", false},
|
|
|
|
// Minimal range (1-512)
|
|
{"minimal min", 1, "minimal", true},
|
|
{"minimal mid", 256, "minimal", true},
|
|
{"minimal max", 512, "minimal", true},
|
|
|
|
// Low range (513-1024)
|
|
{"low start", 513, "low", true},
|
|
{"low boundary", 1024, "low", true},
|
|
|
|
// Medium range (1025-8192)
|
|
{"medium start", 1025, "medium", true},
|
|
{"medium mid", 4096, "medium", true},
|
|
{"medium boundary", 8192, "medium", true},
|
|
|
|
// High range (8193-24576)
|
|
{"high start", 8193, "high", true},
|
|
{"high mid", 16384, "high", true},
|
|
{"high boundary", 24576, "high", true},
|
|
|
|
// XHigh range (24577+)
|
|
{"xhigh start", 24577, "xhigh", true},
|
|
{"xhigh mid", 32768, "xhigh", true},
|
|
{"xhigh large", 100000, "xhigh", true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
level, ok := ConvertBudgetToLevel(tt.budget)
|
|
if ok != tt.wantOK {
|
|
t.Errorf("ConvertBudgetToLevel(%d) ok = %v, want %v", tt.budget, ok, tt.wantOK)
|
|
}
|
|
if level != tt.want {
|
|
t.Errorf("ConvertBudgetToLevel(%d) = %q, want %q", tt.budget, level, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestConvertMixedFormat tests mixed format handling.
|
|
//
|
|
// Tests scenarios where both level and budget might be present,
|
|
// or where format conversion requires special handling.
|
|
//
|
|
// Depends on: Epic 4 Story 4-3 (mixed format handling)
|
|
func TestConvertMixedFormat(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
inputBudget int
|
|
inputLevel string
|
|
wantMode ThinkingMode
|
|
wantBudget int
|
|
wantLevel ThinkingLevel
|
|
}{
|
|
// Level takes precedence when both present
|
|
{"level and budget - level wins", 8192, "high", ModeLevel, 0, LevelHigh},
|
|
{"level and zero budget", 0, "high", ModeLevel, 0, LevelHigh},
|
|
|
|
// Budget only
|
|
{"budget only", 16384, "", ModeBudget, 16384, ""},
|
|
|
|
// Level only
|
|
{"level only", 0, "medium", ModeLevel, 0, LevelMedium},
|
|
|
|
// Neither (default)
|
|
{"neither", 0, "", ModeNone, 0, ""},
|
|
|
|
// Special values
|
|
{"auto level", 0, "auto", ModeAuto, -1, LevelAuto},
|
|
{"none level", 0, "none", ModeNone, 0, LevelNone},
|
|
{"auto budget", -1, "", ModeAuto, -1, ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := normalizeMixedConfig(tt.inputBudget, tt.inputLevel)
|
|
if got.Mode != tt.wantMode {
|
|
t.Errorf("normalizeMixedConfig(%d, %q) Mode = %v, want %v", tt.inputBudget, tt.inputLevel, got.Mode, tt.wantMode)
|
|
}
|
|
if got.Budget != tt.wantBudget {
|
|
t.Errorf("normalizeMixedConfig(%d, %q) Budget = %d, want %d", tt.inputBudget, tt.inputLevel, got.Budget, tt.wantBudget)
|
|
}
|
|
if got.Level != tt.wantLevel {
|
|
t.Errorf("normalizeMixedConfig(%d, %q) Level = %q, want %q", tt.inputBudget, tt.inputLevel, got.Level, tt.wantLevel)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestNormalizeForModel tests model-aware format normalization.
|
|
func TestNormalizeForModel(t *testing.T) {
|
|
budgetOnlyModel := ®istry.ModelInfo{
|
|
Thinking: ®istry.ThinkingSupport{
|
|
Min: 1024,
|
|
Max: 128000,
|
|
},
|
|
}
|
|
levelOnlyModel := ®istry.ModelInfo{
|
|
Thinking: ®istry.ThinkingSupport{
|
|
Levels: []string{"low", "medium", "high"},
|
|
},
|
|
}
|
|
hybridModel := ®istry.ModelInfo{
|
|
Thinking: ®istry.ThinkingSupport{
|
|
Min: 128,
|
|
Max: 32768,
|
|
Levels: []string{"minimal", "low", "medium", "high"},
|
|
},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
config ThinkingConfig
|
|
model *registry.ModelInfo
|
|
want ThinkingConfig
|
|
wantErr bool
|
|
}{
|
|
{"budget-only keeps budget", ThinkingConfig{Mode: ModeBudget, Budget: 8192}, budgetOnlyModel, ThinkingConfig{Mode: ModeBudget, Budget: 8192}, false},
|
|
{"budget-only converts level", ThinkingConfig{Mode: ModeLevel, Level: LevelHigh}, budgetOnlyModel, ThinkingConfig{Mode: ModeBudget, Budget: 24576}, false},
|
|
{"level-only converts budget", ThinkingConfig{Mode: ModeBudget, Budget: 8192}, levelOnlyModel, ThinkingConfig{Mode: ModeLevel, Level: LevelMedium}, false},
|
|
{"level-only keeps level", ThinkingConfig{Mode: ModeLevel, Level: LevelLow}, levelOnlyModel, ThinkingConfig{Mode: ModeLevel, Level: LevelLow}, false},
|
|
{"hybrid keeps budget", ThinkingConfig{Mode: ModeBudget, Budget: 16384}, hybridModel, ThinkingConfig{Mode: ModeBudget, Budget: 16384}, false},
|
|
{"hybrid keeps level", ThinkingConfig{Mode: ModeLevel, Level: LevelMinimal}, hybridModel, ThinkingConfig{Mode: ModeLevel, Level: LevelMinimal}, false},
|
|
{"auto passthrough", ThinkingConfig{Mode: ModeAuto, Budget: -1}, levelOnlyModel, ThinkingConfig{Mode: ModeAuto, Budget: -1}, false},
|
|
{"none passthrough", ThinkingConfig{Mode: ModeNone, Budget: 0}, budgetOnlyModel, ThinkingConfig{Mode: ModeNone, Budget: 0}, false},
|
|
{"invalid level", ThinkingConfig{Mode: ModeLevel, Level: "ultra"}, budgetOnlyModel, ThinkingConfig{}, true},
|
|
{"invalid budget", ThinkingConfig{Mode: ModeBudget, Budget: -2}, levelOnlyModel, ThinkingConfig{}, true},
|
|
{"nil modelInfo passthrough budget", ThinkingConfig{Mode: ModeBudget, Budget: 8192}, nil, ThinkingConfig{Mode: ModeBudget, Budget: 8192}, false},
|
|
{"nil modelInfo passthrough level", ThinkingConfig{Mode: ModeLevel, Level: LevelHigh}, nil, ThinkingConfig{Mode: ModeLevel, Level: LevelHigh}, false},
|
|
{"nil thinking degrades to none", ThinkingConfig{Mode: ModeBudget, Budget: 4096}, ®istry.ModelInfo{}, ThinkingConfig{Mode: ModeNone, Budget: 0}, false},
|
|
{"nil thinking level degrades to none", ThinkingConfig{Mode: ModeLevel, Level: LevelHigh}, ®istry.ModelInfo{}, ThinkingConfig{Mode: ModeNone, Budget: 0}, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := NormalizeForModel(&tt.config, tt.model)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Fatalf("NormalizeForModel(%+v) error = %v, wantErr %v", tt.config, err, tt.wantErr)
|
|
}
|
|
if tt.wantErr {
|
|
return
|
|
}
|
|
if got == nil {
|
|
t.Fatalf("NormalizeForModel(%+v) returned nil config", tt.config)
|
|
}
|
|
if got.Mode != tt.want.Mode {
|
|
t.Errorf("NormalizeForModel(%+v) Mode = %v, want %v", tt.config, got.Mode, tt.want.Mode)
|
|
}
|
|
if got.Budget != tt.want.Budget {
|
|
t.Errorf("NormalizeForModel(%+v) Budget = %d, want %d", tt.config, got.Budget, tt.want.Budget)
|
|
}
|
|
if got.Level != tt.want.Level {
|
|
t.Errorf("NormalizeForModel(%+v) Level = %q, want %q", tt.config, got.Level, tt.want.Level)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestLevelToBudgetRoundTrip tests level → budget → level round trip.
|
|
//
|
|
// Verifies that converting level to budget and back produces consistent results.
|
|
//
|
|
// Depends on: Epic 4 Story 4-1, 4-2
|
|
func TestLevelToBudgetRoundTrip(t *testing.T) {
|
|
levels := []string{"none", "auto", "minimal", "low", "medium", "high", "xhigh"}
|
|
|
|
for _, level := range levels {
|
|
t.Run(level, func(t *testing.T) {
|
|
budget, ok := ConvertLevelToBudget(level)
|
|
if !ok {
|
|
t.Fatalf("ConvertLevelToBudget(%q) returned ok=false", level)
|
|
}
|
|
resultLevel, ok := ConvertBudgetToLevel(budget)
|
|
if !ok {
|
|
t.Fatalf("ConvertBudgetToLevel(%d) returned ok=false", budget)
|
|
}
|
|
if resultLevel != level {
|
|
t.Errorf("round trip: %q → %d → %q, want %q", level, budget, resultLevel, level)
|
|
}
|
|
})
|
|
}
|
|
}
|