mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-18 12:20:52 +08:00
344 lines
11 KiB
Go
344 lines
11 KiB
Go
// Package openai implements thinking configuration for OpenAI/Codex models.
|
|
package openai
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
|
"github.com/tidwall/gjson"
|
|
)
|
|
|
|
func buildOpenAIModelInfo(modelID string) *registry.ModelInfo {
|
|
info := registry.LookupStaticModelInfo(modelID)
|
|
if info != nil {
|
|
return info
|
|
}
|
|
// Fallback with complete ThinkingSupport matching real OpenAI model capabilities
|
|
return ®istry.ModelInfo{
|
|
ID: modelID,
|
|
Thinking: ®istry.ThinkingSupport{
|
|
Min: 1024,
|
|
Max: 32768,
|
|
ZeroAllowed: true,
|
|
Levels: []string{"none", "low", "medium", "high", "xhigh"},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestNewApplier(t *testing.T) {
|
|
applier := NewApplier()
|
|
if applier == nil {
|
|
t.Fatalf("expected non-nil applier")
|
|
}
|
|
}
|
|
|
|
func TestApplierImplementsInterface(t *testing.T) {
|
|
_, ok := interface{}(NewApplier()).(thinking.ProviderApplier)
|
|
if !ok {
|
|
t.Fatalf("expected Applier to implement thinking.ProviderApplier")
|
|
}
|
|
}
|
|
|
|
func TestApplyNilModelInfo(t *testing.T) {
|
|
applier := NewApplier()
|
|
body := []byte(`{"model":"gpt-5.2"}`)
|
|
got, err := applier.Apply(body, thinking.ThinkingConfig{}, nil)
|
|
if err != nil {
|
|
t.Fatalf("expected nil error, got %v", err)
|
|
}
|
|
if string(got) != string(body) {
|
|
t.Fatalf("expected body unchanged, got %s", string(got))
|
|
}
|
|
}
|
|
|
|
func TestApplyMissingThinkingSupport(t *testing.T) {
|
|
applier := NewApplier()
|
|
modelInfo := ®istry.ModelInfo{ID: "gpt-5.2"}
|
|
got, err := applier.Apply([]byte(`{"model":"gpt-5.2"}`), thinking.ThinkingConfig{}, modelInfo)
|
|
if err == nil {
|
|
t.Fatalf("expected error, got nil")
|
|
}
|
|
if got != nil {
|
|
t.Fatalf("expected nil body on error, got %s", string(got))
|
|
}
|
|
thinkingErr, ok := err.(*thinking.ThinkingError)
|
|
if !ok {
|
|
t.Fatalf("expected ThinkingError, got %T", err)
|
|
}
|
|
if thinkingErr.Code != thinking.ErrThinkingNotSupported {
|
|
t.Fatalf("expected code %s, got %s", thinking.ErrThinkingNotSupported, thinkingErr.Code)
|
|
}
|
|
if thinkingErr.Model != "gpt-5.2" {
|
|
t.Fatalf("expected model gpt-5.2, got %s", thinkingErr.Model)
|
|
}
|
|
}
|
|
|
|
// TestApplyLevel tests Apply with ModeLevel (unit test, no ValidateConfig).
|
|
func TestApplyLevel(t *testing.T) {
|
|
applier := NewApplier()
|
|
modelInfo := buildOpenAIModelInfo("gpt-5.2")
|
|
|
|
tests := []struct {
|
|
name string
|
|
level thinking.ThinkingLevel
|
|
want string
|
|
}{
|
|
{"high", thinking.LevelHigh, "high"},
|
|
{"medium", thinking.LevelMedium, "medium"},
|
|
{"low", thinking.LevelLow, "low"},
|
|
{"xhigh", thinking.LevelXHigh, "xhigh"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, err := applier.Apply([]byte(`{}`), thinking.ThinkingConfig{Mode: thinking.ModeLevel, Level: tt.level}, modelInfo)
|
|
if err != nil {
|
|
t.Fatalf("Apply() error = %v", err)
|
|
}
|
|
if got := gjson.GetBytes(result, "reasoning_effort").String(); got != tt.want {
|
|
t.Fatalf("reasoning_effort = %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestApplyModeNone tests Apply with ModeNone (unit test).
|
|
func TestApplyModeNone(t *testing.T) {
|
|
applier := NewApplier()
|
|
|
|
tests := []struct {
|
|
name string
|
|
config thinking.ThinkingConfig
|
|
modelInfo *registry.ModelInfo
|
|
want string
|
|
}{
|
|
{"zero allowed", thinking.ThinkingConfig{Mode: thinking.ModeNone, Budget: 0}, ®istry.ModelInfo{ID: "gpt-5.2", Thinking: ®istry.ThinkingSupport{ZeroAllowed: true, Levels: []string{"none", "low"}}}, "none"},
|
|
{"clamped to level", thinking.ThinkingConfig{Mode: thinking.ModeNone, Budget: 128, Level: thinking.LevelLow}, ®istry.ModelInfo{ID: "gpt-5", Thinking: ®istry.ThinkingSupport{Levels: []string{"minimal", "low"}}}, "low"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, err := applier.Apply([]byte(`{}`), tt.config, tt.modelInfo)
|
|
if err != nil {
|
|
t.Fatalf("Apply() error = %v", err)
|
|
}
|
|
if got := gjson.GetBytes(result, "reasoning_effort").String(); got != tt.want {
|
|
t.Fatalf("reasoning_effort = %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestApplyPassthrough tests that unsupported modes pass through unchanged.
|
|
func TestApplyPassthrough(t *testing.T) {
|
|
applier := NewApplier()
|
|
modelInfo := buildOpenAIModelInfo("gpt-5.2")
|
|
|
|
tests := []struct {
|
|
name string
|
|
config thinking.ThinkingConfig
|
|
}{
|
|
{"mode auto", thinking.ThinkingConfig{Mode: thinking.ModeAuto}},
|
|
{"mode budget", thinking.ThinkingConfig{Mode: thinking.ModeBudget, Budget: 8192}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
body := []byte(`{"model":"gpt-5.2"}`)
|
|
result, err := applier.Apply(body, tt.config, modelInfo)
|
|
if err != nil {
|
|
t.Fatalf("Apply() error = %v", err)
|
|
}
|
|
if string(result) != string(body) {
|
|
t.Fatalf("Apply() result = %s, want %s", string(result), string(body))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestApplyInvalidBody tests Apply with invalid body input.
|
|
func TestApplyInvalidBody(t *testing.T) {
|
|
applier := NewApplier()
|
|
modelInfo := buildOpenAIModelInfo("gpt-5.2")
|
|
|
|
tests := []struct {
|
|
name string
|
|
body []byte
|
|
}{
|
|
{"nil body", nil},
|
|
{"empty body", []byte{}},
|
|
{"invalid json", []byte(`{"not json"`)},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, err := applier.Apply(tt.body, thinking.ThinkingConfig{Mode: thinking.ModeLevel, Level: thinking.LevelHigh}, modelInfo)
|
|
if err != nil {
|
|
t.Fatalf("Apply() error = %v", err)
|
|
}
|
|
if !gjson.ValidBytes(result) {
|
|
t.Fatalf("Apply() result is not valid JSON: %s", string(result))
|
|
}
|
|
if got := gjson.GetBytes(result, "reasoning_effort").String(); got != "high" {
|
|
t.Fatalf("reasoning_effort = %q, want %q", got, "high")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestApplyPreservesFields tests that existing body fields are preserved.
|
|
func TestApplyPreservesFields(t *testing.T) {
|
|
applier := NewApplier()
|
|
modelInfo := buildOpenAIModelInfo("gpt-5.2")
|
|
|
|
body := []byte(`{"model":"gpt-5.2","messages":[]}`)
|
|
result, err := applier.Apply(body, thinking.ThinkingConfig{Mode: thinking.ModeLevel, Level: thinking.LevelLow}, modelInfo)
|
|
if err != nil {
|
|
t.Fatalf("Apply() error = %v", err)
|
|
}
|
|
if got := gjson.GetBytes(result, "model").String(); got != "gpt-5.2" {
|
|
t.Fatalf("model = %q, want %q", got, "gpt-5.2")
|
|
}
|
|
if !gjson.GetBytes(result, "messages").Exists() {
|
|
t.Fatalf("messages missing from result: %s", string(result))
|
|
}
|
|
if got := gjson.GetBytes(result, "reasoning_effort").String(); got != "low" {
|
|
t.Fatalf("reasoning_effort = %q, want %q", got, "low")
|
|
}
|
|
}
|
|
|
|
// TestHasLevel tests the hasLevel helper function.
|
|
func TestHasLevel(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
levels []string
|
|
target string
|
|
want bool
|
|
}{
|
|
{"exact match", []string{"low", "medium", "high"}, "medium", true},
|
|
{"case insensitive", []string{"low", "medium", "high"}, "MEDIUM", true},
|
|
{"with spaces", []string{"low", " medium ", "high"}, "medium", true},
|
|
{"not found", []string{"low", "medium", "high"}, "xhigh", false},
|
|
{"empty levels", []string{}, "medium", false},
|
|
{"none level", []string{"none", "low", "medium"}, "none", true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := hasLevel(tt.levels, tt.target); got != tt.want {
|
|
t.Fatalf("hasLevel(%v, %q) = %v, want %v", tt.levels, tt.target, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// --- End-to-End Tests (ValidateConfig → Apply) ---
|
|
|
|
// TestE2EApply tests the full flow: ValidateConfig → Apply.
|
|
func TestE2EApply(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
model string
|
|
config thinking.ThinkingConfig
|
|
want string
|
|
}{
|
|
{"level high", "gpt-5.2", thinking.ThinkingConfig{Mode: thinking.ModeLevel, Level: thinking.LevelHigh}, "high"},
|
|
{"level medium", "gpt-5.2", thinking.ThinkingConfig{Mode: thinking.ModeLevel, Level: thinking.LevelMedium}, "medium"},
|
|
{"level low", "gpt-5.2", thinking.ThinkingConfig{Mode: thinking.ModeLevel, Level: thinking.LevelLow}, "low"},
|
|
{"level xhigh", "gpt-5.2", thinking.ThinkingConfig{Mode: thinking.ModeLevel, Level: thinking.LevelXHigh}, "xhigh"},
|
|
{"mode none", "gpt-5.2", thinking.ThinkingConfig{Mode: thinking.ModeNone, Budget: 0}, "none"},
|
|
{"budget to level", "gpt-5.2", thinking.ThinkingConfig{Mode: thinking.ModeBudget, Budget: 8192}, "medium"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
modelInfo := buildOpenAIModelInfo(tt.model)
|
|
normalized, err := thinking.ValidateConfig(tt.config, modelInfo.Thinking)
|
|
if err != nil {
|
|
t.Fatalf("ValidateConfig() error = %v", err)
|
|
}
|
|
|
|
applier := NewApplier()
|
|
result, err := applier.Apply([]byte(`{}`), *normalized, modelInfo)
|
|
if err != nil {
|
|
t.Fatalf("Apply() error = %v", err)
|
|
}
|
|
if got := gjson.GetBytes(result, "reasoning_effort").String(); got != tt.want {
|
|
t.Fatalf("reasoning_effort = %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestE2EApplyOutputFormat tests the full flow with exact JSON output verification.
|
|
func TestE2EApplyOutputFormat(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
model string
|
|
config thinking.ThinkingConfig
|
|
wantJSON string
|
|
}{
|
|
{"level high", "gpt-5.2", thinking.ThinkingConfig{Mode: thinking.ModeLevel, Level: thinking.LevelHigh}, `{"reasoning_effort":"high"}`},
|
|
{"level none", "gpt-5.2", thinking.ThinkingConfig{Mode: thinking.ModeNone, Budget: 0}, `{"reasoning_effort":"none"}`},
|
|
{"budget converted", "gpt-5.2", thinking.ThinkingConfig{Mode: thinking.ModeBudget, Budget: 8192}, `{"reasoning_effort":"medium"}`},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
modelInfo := buildOpenAIModelInfo(tt.model)
|
|
normalized, err := thinking.ValidateConfig(tt.config, modelInfo.Thinking)
|
|
if err != nil {
|
|
t.Fatalf("ValidateConfig() error = %v", err)
|
|
}
|
|
|
|
applier := NewApplier()
|
|
result, err := applier.Apply([]byte(`{}`), *normalized, modelInfo)
|
|
if err != nil {
|
|
t.Fatalf("Apply() error = %v", err)
|
|
}
|
|
if string(result) != tt.wantJSON {
|
|
t.Fatalf("Apply() result = %s, want %s", string(result), tt.wantJSON)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestE2EApplyWithExistingBody tests the full flow with existing body fields.
|
|
func TestE2EApplyWithExistingBody(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
body string
|
|
config thinking.ThinkingConfig
|
|
wantEffort string
|
|
wantModel string
|
|
}{
|
|
{"empty body", `{}`, thinking.ThinkingConfig{Mode: thinking.ModeLevel, Level: thinking.LevelHigh}, "high", ""},
|
|
{"preserve fields", `{"model":"gpt-5.2","messages":[]}`, thinking.ThinkingConfig{Mode: thinking.ModeLevel, Level: thinking.LevelMedium}, "medium", "gpt-5.2"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
modelInfo := buildOpenAIModelInfo("gpt-5.2")
|
|
normalized, err := thinking.ValidateConfig(tt.config, modelInfo.Thinking)
|
|
if err != nil {
|
|
t.Fatalf("ValidateConfig() error = %v", err)
|
|
}
|
|
|
|
applier := NewApplier()
|
|
result, err := applier.Apply([]byte(tt.body), *normalized, modelInfo)
|
|
if err != nil {
|
|
t.Fatalf("Apply() error = %v", err)
|
|
}
|
|
if got := gjson.GetBytes(result, "reasoning_effort").String(); got != tt.wantEffort {
|
|
t.Fatalf("reasoning_effort = %q, want %q", got, tt.wantEffort)
|
|
}
|
|
if tt.wantModel != "" {
|
|
if got := gjson.GetBytes(result, "model").String(); got != tt.wantModel {
|
|
t.Fatalf("model = %q, want %q", got, tt.wantModel)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|