mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-02 04:20:50 +08:00
289 lines
9.1 KiB
Go
289 lines
9.1 KiB
Go
// Package claude implements thinking configuration for Claude models.
|
|
package claude
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
|
"github.com/tidwall/gjson"
|
|
)
|
|
|
|
// =============================================================================
|
|
// Unit Tests: Applier Creation and Interface
|
|
// =============================================================================
|
|
|
|
func TestNewApplier(t *testing.T) {
|
|
applier := NewApplier()
|
|
if applier == nil {
|
|
t.Fatal("NewApplier() returned nil")
|
|
}
|
|
}
|
|
|
|
func TestApplierImplementsInterface(t *testing.T) {
|
|
var _ thinking.ProviderApplier = (*Applier)(nil)
|
|
}
|
|
|
|
// =============================================================================
|
|
// Unit Tests: Budget and Disable Logic (Pre-validated Config)
|
|
// =============================================================================
|
|
|
|
// TestClaudeApplyBudgetAndNone tests budget values and disable modes.
|
|
// NOTE: These tests assume config has been pre-validated by ValidateConfig.
|
|
// Apply trusts the input and does not perform clamping.
|
|
func TestClaudeApplyBudgetAndNone(t *testing.T) {
|
|
applier := NewApplier()
|
|
modelInfo := buildClaudeModelInfo()
|
|
|
|
tests := []struct {
|
|
name string
|
|
config thinking.ThinkingConfig
|
|
wantType string
|
|
wantBudget int
|
|
wantBudgetOK bool
|
|
}{
|
|
// Valid pre-validated budget values
|
|
{"budget 16k", thinking.ThinkingConfig{Mode: thinking.ModeBudget, Budget: 16384}, "enabled", 16384, true},
|
|
{"budget min", thinking.ThinkingConfig{Mode: thinking.ModeBudget, Budget: 1024}, "enabled", 1024, true},
|
|
{"budget max", thinking.ThinkingConfig{Mode: thinking.ModeBudget, Budget: 128000}, "enabled", 128000, true},
|
|
{"budget mid", thinking.ThinkingConfig{Mode: thinking.ModeBudget, Budget: 50000}, "enabled", 50000, true},
|
|
// Disable cases
|
|
{"budget zero disables", thinking.ThinkingConfig{Mode: thinking.ModeBudget, Budget: 0}, "disabled", 0, false},
|
|
{"mode none disables", thinking.ThinkingConfig{Mode: thinking.ModeNone, Budget: 0}, "disabled", 0, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, err := applier.Apply([]byte(`{}`), tt.config, modelInfo)
|
|
if err != nil {
|
|
t.Fatalf("Apply() error = %v", err)
|
|
}
|
|
|
|
thinkingType := gjson.GetBytes(result, "thinking.type").String()
|
|
if thinkingType != tt.wantType {
|
|
t.Fatalf("thinking.type = %q, want %q", thinkingType, tt.wantType)
|
|
}
|
|
|
|
budgetValue := gjson.GetBytes(result, "thinking.budget_tokens")
|
|
if budgetValue.Exists() != tt.wantBudgetOK {
|
|
t.Fatalf("thinking.budget_tokens exists = %v, want %v", budgetValue.Exists(), tt.wantBudgetOK)
|
|
}
|
|
if tt.wantBudgetOK {
|
|
if got := int(budgetValue.Int()); got != tt.wantBudget {
|
|
t.Fatalf("thinking.budget_tokens = %d, want %d", got, tt.wantBudget)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestClaudeApplyPassthroughBudget tests that Apply trusts pre-validated budget values.
|
|
// It does NOT perform clamping - that's ValidateConfig's responsibility.
|
|
func TestClaudeApplyPassthroughBudget(t *testing.T) {
|
|
applier := NewApplier()
|
|
modelInfo := buildClaudeModelInfo()
|
|
|
|
tests := []struct {
|
|
name string
|
|
config thinking.ThinkingConfig
|
|
wantBudget int
|
|
}{
|
|
// Apply should pass through the budget value as-is
|
|
// (ValidateConfig would have clamped these, but Apply trusts the input)
|
|
{"passes through any budget", thinking.ThinkingConfig{Mode: thinking.ModeBudget, Budget: 500}, 500},
|
|
{"passes through large budget", thinking.ThinkingConfig{Mode: thinking.ModeBudget, Budget: 200000}, 200000},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, err := applier.Apply([]byte(`{}`), tt.config, modelInfo)
|
|
if err != nil {
|
|
t.Fatalf("Apply() error = %v", err)
|
|
}
|
|
|
|
if got := int(gjson.GetBytes(result, "thinking.budget_tokens").Int()); got != tt.wantBudget {
|
|
t.Fatalf("thinking.budget_tokens = %d, want %d (passthrough)", got, tt.wantBudget)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Unit Tests: Mode Passthrough (Strict Layering)
|
|
// =============================================================================
|
|
|
|
// TestClaudeApplyModePassthrough tests that non-Budget/None modes pass through unchanged.
|
|
// Apply expects ValidateConfig to have already converted Level/Auto to Budget.
|
|
func TestClaudeApplyModePassthrough(t *testing.T) {
|
|
applier := NewApplier()
|
|
modelInfo := buildClaudeModelInfo()
|
|
|
|
tests := []struct {
|
|
name string
|
|
config thinking.ThinkingConfig
|
|
body string
|
|
}{
|
|
{"ModeLevel passes through", thinking.ThinkingConfig{Mode: thinking.ModeLevel, Level: "high"}, `{"model":"test"}`},
|
|
{"ModeAuto passes through", thinking.ThinkingConfig{Mode: thinking.ModeAuto, Budget: -1}, `{"model":"test"}`},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, err := applier.Apply([]byte(tt.body), tt.config, modelInfo)
|
|
if err != nil {
|
|
t.Fatalf("Apply() error = %v", err)
|
|
}
|
|
|
|
// Should return body unchanged
|
|
if string(result) != tt.body {
|
|
t.Fatalf("Apply() = %s, want %s (passthrough)", string(result), tt.body)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Unit Tests: Output Format
|
|
// =============================================================================
|
|
|
|
// TestClaudeApplyOutputFormat tests the exact JSON output format.
|
|
//
|
|
// Claude expects:
|
|
//
|
|
// {
|
|
// "thinking": {
|
|
// "type": "enabled",
|
|
// "budget_tokens": 16384
|
|
// }
|
|
// }
|
|
func TestClaudeApplyOutputFormat(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config thinking.ThinkingConfig
|
|
wantJSON string
|
|
}{
|
|
{
|
|
"enabled with budget",
|
|
thinking.ThinkingConfig{Mode: thinking.ModeBudget, Budget: 16384},
|
|
`{"thinking":{"type":"enabled","budget_tokens":16384}}`,
|
|
},
|
|
{
|
|
"disabled",
|
|
thinking.ThinkingConfig{Mode: thinking.ModeNone, Budget: 0},
|
|
`{"thinking":{"type":"disabled"}}`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
applier := NewApplier()
|
|
modelInfo := buildClaudeModelInfo()
|
|
|
|
result, err := applier.Apply([]byte(`{}`), tt.config, modelInfo)
|
|
if err != nil {
|
|
t.Fatalf("Apply() error = %v", err)
|
|
}
|
|
if string(result) != tt.wantJSON {
|
|
t.Fatalf("Apply() = %s, want %s", result, tt.wantJSON)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Unit Tests: Body Merging
|
|
// =============================================================================
|
|
|
|
// TestClaudeApplyWithExistingBody tests applying config to existing request body.
|
|
func TestClaudeApplyWithExistingBody(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
body string
|
|
config thinking.ThinkingConfig
|
|
wantBody string
|
|
}{
|
|
{
|
|
"add to empty body",
|
|
`{}`,
|
|
thinking.ThinkingConfig{Mode: thinking.ModeBudget, Budget: 16384},
|
|
`{"thinking":{"type":"enabled","budget_tokens":16384}}`,
|
|
},
|
|
{
|
|
"preserve existing fields",
|
|
`{"model":"claude-sonnet-4-5","messages":[]}`,
|
|
thinking.ThinkingConfig{Mode: thinking.ModeBudget, Budget: 8192},
|
|
`{"model":"claude-sonnet-4-5","messages":[],"thinking":{"type":"enabled","budget_tokens":8192}}`,
|
|
},
|
|
{
|
|
"override existing thinking",
|
|
`{"thinking":{"type":"enabled","budget_tokens":1000}}`,
|
|
thinking.ThinkingConfig{Mode: thinking.ModeBudget, Budget: 16384},
|
|
`{"thinking":{"type":"enabled","budget_tokens":16384}}`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
applier := NewApplier()
|
|
modelInfo := buildClaudeModelInfo()
|
|
|
|
result, err := applier.Apply([]byte(tt.body), tt.config, modelInfo)
|
|
if err != nil {
|
|
t.Fatalf("Apply() error = %v", err)
|
|
}
|
|
if string(result) != tt.wantBody {
|
|
t.Fatalf("Apply() = %s, want %s", result, tt.wantBody)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestClaudeApplyWithNilBody tests handling of nil/empty body.
|
|
func TestClaudeApplyWithNilBody(t *testing.T) {
|
|
applier := NewApplier()
|
|
modelInfo := buildClaudeModelInfo()
|
|
|
|
tests := []struct {
|
|
name string
|
|
body []byte
|
|
wantBudget int
|
|
}{
|
|
{"nil body", nil, 16384},
|
|
{"empty body", []byte{}, 16384},
|
|
{"empty object", []byte(`{}`), 16384},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
config := thinking.ThinkingConfig{Mode: thinking.ModeBudget, Budget: 16384}
|
|
result, err := applier.Apply(tt.body, config, modelInfo)
|
|
if err != nil {
|
|
t.Fatalf("Apply() error = %v", err)
|
|
}
|
|
|
|
if got := gjson.GetBytes(result, "thinking.type").String(); got != "enabled" {
|
|
t.Fatalf("thinking.type = %q, want %q", got, "enabled")
|
|
}
|
|
if got := int(gjson.GetBytes(result, "thinking.budget_tokens").Int()); got != tt.wantBudget {
|
|
t.Fatalf("thinking.budget_tokens = %d, want %d", got, tt.wantBudget)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Helper Functions
|
|
// =============================================================================
|
|
|
|
func buildClaudeModelInfo() *registry.ModelInfo {
|
|
return ®istry.ModelInfo{
|
|
ID: "claude-sonnet-4-5",
|
|
Thinking: ®istry.ThinkingSupport{
|
|
Min: 1024,
|
|
Max: 128000,
|
|
ZeroAllowed: true,
|
|
DynamicAllowed: false,
|
|
},
|
|
}
|
|
}
|