mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-18 12:20:52 +08:00
329 lines
11 KiB
Go
329 lines
11 KiB
Go
// Package iflow implements thinking configuration for iFlow models (GLM, MiniMax).
|
|
package iflow
|
|
|
|
import (
|
|
"bytes"
|
|
"testing"
|
|
|
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
|
"github.com/tidwall/gjson"
|
|
)
|
|
|
|
func TestNewApplier(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
}{
|
|
{"default"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
applier := NewApplier()
|
|
if applier == nil {
|
|
t.Fatalf("expected non-nil applier")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestApplierImplementsInterface(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
applier thinking.ProviderApplier
|
|
}{
|
|
{"default", NewApplier()},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if tt.applier == nil {
|
|
t.Fatalf("expected thinking.ProviderApplier implementation")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestApplyNilModelInfo(t *testing.T) {
|
|
applier := NewApplier()
|
|
|
|
tests := []struct {
|
|
name string
|
|
body []byte
|
|
}{
|
|
{"nil body", nil},
|
|
{"empty body", []byte{}},
|
|
{"json body", []byte(`{"model":"glm-4.6"}`)},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := applier.Apply(tt.body, thinking.ThinkingConfig{}, nil)
|
|
if err != nil {
|
|
t.Fatalf("expected nil error, got %v", err)
|
|
}
|
|
if !bytes.Equal(got, tt.body) {
|
|
t.Fatalf("expected body unchanged, got %s", string(got))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestApplyMissingThinkingSupport(t *testing.T) {
|
|
applier := NewApplier()
|
|
|
|
tests := []struct {
|
|
name string
|
|
modelID string
|
|
wantModel string
|
|
}{
|
|
{"model id", "glm-4.6", "glm-4.6"},
|
|
{"empty model id", "", "unknown"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
modelInfo := ®istry.ModelInfo{ID: tt.modelID}
|
|
got, err := applier.Apply([]byte(`{"model":"`+tt.modelID+`"}`), 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 != tt.wantModel {
|
|
t.Fatalf("expected model %s, got %s", tt.wantModel, thinkingErr.Model)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConfigToBoolean(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config thinking.ThinkingConfig
|
|
want bool
|
|
}{
|
|
{"mode none", thinking.ThinkingConfig{Mode: thinking.ModeNone}, false},
|
|
{"mode auto", thinking.ThinkingConfig{Mode: thinking.ModeAuto}, true},
|
|
{"budget zero", thinking.ThinkingConfig{Mode: thinking.ModeBudget, Budget: 0}, false},
|
|
{"budget positive", thinking.ThinkingConfig{Mode: thinking.ModeBudget, Budget: 1000}, true},
|
|
{"level none", thinking.ThinkingConfig{Mode: thinking.ModeLevel, Level: thinking.LevelNone}, false},
|
|
{"level minimal", thinking.ThinkingConfig{Mode: thinking.ModeLevel, Level: thinking.LevelMinimal}, true},
|
|
{"level low", thinking.ThinkingConfig{Mode: thinking.ModeLevel, Level: thinking.LevelLow}, true},
|
|
{"level medium", thinking.ThinkingConfig{Mode: thinking.ModeLevel, Level: thinking.LevelMedium}, true},
|
|
{"level high", thinking.ThinkingConfig{Mode: thinking.ModeLevel, Level: thinking.LevelHigh}, true},
|
|
{"level xhigh", thinking.ThinkingConfig{Mode: thinking.ModeLevel, Level: thinking.LevelXHigh}, true},
|
|
{"zero value config", thinking.ThinkingConfig{}, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := configToBoolean(tt.config); got != tt.want {
|
|
t.Fatalf("configToBoolean(%+v) = %v, want %v", tt.config, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestApplyGLM(t *testing.T) {
|
|
applier := NewApplier()
|
|
|
|
tests := []struct {
|
|
name string
|
|
modelID string
|
|
body []byte
|
|
config thinking.ThinkingConfig
|
|
wantEnable bool
|
|
wantPreserve string
|
|
}{
|
|
{"mode none", "glm-4.6", []byte(`{}`), thinking.ThinkingConfig{Mode: thinking.ModeNone}, false, ""},
|
|
{"level none", "glm-4.7", []byte(`{}`), thinking.ThinkingConfig{Mode: thinking.ModeLevel, Level: thinking.LevelNone}, false, ""},
|
|
{"mode auto", "glm-4.6", []byte(`{}`), thinking.ThinkingConfig{Mode: thinking.ModeAuto}, true, ""},
|
|
{"level minimal", "glm-4.6", []byte(`{}`), thinking.ThinkingConfig{Mode: thinking.ModeLevel, Level: thinking.LevelMinimal}, true, ""},
|
|
{"level low", "glm-4.7", []byte(`{}`), thinking.ThinkingConfig{Mode: thinking.ModeLevel, Level: thinking.LevelLow}, true, ""},
|
|
{"level medium", "glm-4.6", []byte(`{}`), thinking.ThinkingConfig{Mode: thinking.ModeLevel, Level: thinking.LevelMedium}, true, ""},
|
|
{"level high", "GLM-4.6", []byte(`{}`), thinking.ThinkingConfig{Mode: thinking.ModeLevel, Level: thinking.LevelHigh}, true, ""},
|
|
{"level xhigh", "glm-z1-preview", []byte(`{}`), thinking.ThinkingConfig{Mode: thinking.ModeLevel, Level: thinking.LevelXHigh}, true, ""},
|
|
{"budget zero", "glm-4.6", []byte(`{}`), thinking.ThinkingConfig{Mode: thinking.ModeBudget, Budget: 0}, false, ""},
|
|
{"budget 1000", "glm-4.6", []byte(`{}`), thinking.ThinkingConfig{Mode: thinking.ModeBudget, Budget: 1000}, true, ""},
|
|
{"preserve fields", "glm-4.6", []byte(`{"model":"glm-4.6","extra":{"keep":true}}`), thinking.ThinkingConfig{Mode: thinking.ModeAuto}, true, "glm-4.6"},
|
|
{"empty body", "glm-4.6", nil, thinking.ThinkingConfig{Mode: thinking.ModeAuto}, true, ""},
|
|
{"malformed json", "glm-4.6", []byte(`{invalid`), thinking.ThinkingConfig{Mode: thinking.ModeAuto}, true, ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
modelInfo := ®istry.ModelInfo{
|
|
ID: tt.modelID,
|
|
Thinking: ®istry.ThinkingSupport{},
|
|
}
|
|
got, err := applier.Apply(tt.body, tt.config, modelInfo)
|
|
if err != nil {
|
|
t.Fatalf("Apply() error = %v", err)
|
|
}
|
|
if !gjson.ValidBytes(got) {
|
|
t.Fatalf("expected valid JSON, got %s", string(got))
|
|
}
|
|
|
|
enableResult := gjson.GetBytes(got, "chat_template_kwargs.enable_thinking")
|
|
if !enableResult.Exists() {
|
|
t.Fatalf("enable_thinking missing")
|
|
}
|
|
gotEnable := enableResult.Bool()
|
|
if gotEnable != tt.wantEnable {
|
|
t.Fatalf("enable_thinking = %v, want %v", gotEnable, tt.wantEnable)
|
|
}
|
|
|
|
// clear_thinking only set when enable_thinking=true
|
|
clearResult := gjson.GetBytes(got, "chat_template_kwargs.clear_thinking")
|
|
if tt.wantEnable {
|
|
if !clearResult.Exists() {
|
|
t.Fatalf("clear_thinking missing when enable_thinking=true")
|
|
}
|
|
if clearResult.Bool() {
|
|
t.Fatalf("clear_thinking = %v, want false", clearResult.Bool())
|
|
}
|
|
} else {
|
|
if clearResult.Exists() {
|
|
t.Fatalf("clear_thinking should not exist when enable_thinking=false")
|
|
}
|
|
}
|
|
|
|
if tt.wantPreserve != "" {
|
|
gotModel := gjson.GetBytes(got, "model").String()
|
|
if gotModel != tt.wantPreserve {
|
|
t.Fatalf("model = %q, want %q", gotModel, tt.wantPreserve)
|
|
}
|
|
if !gjson.GetBytes(got, "extra.keep").Bool() {
|
|
t.Fatalf("expected extra.keep preserved")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestApplyMiniMax(t *testing.T) {
|
|
applier := NewApplier()
|
|
|
|
tests := []struct {
|
|
name string
|
|
modelID string
|
|
body []byte
|
|
config thinking.ThinkingConfig
|
|
wantSplit bool
|
|
wantModel string
|
|
wantKeep bool
|
|
}{
|
|
{"mode none", "minimax-m2", []byte(`{}`), thinking.ThinkingConfig{Mode: thinking.ModeNone}, false, "", false},
|
|
{"level none", "minimax-m2.1", []byte(`{}`), thinking.ThinkingConfig{Mode: thinking.ModeLevel, Level: thinking.LevelNone}, false, "", false},
|
|
{"mode auto", "minimax-m2", []byte(`{}`), thinking.ThinkingConfig{Mode: thinking.ModeAuto}, true, "", false},
|
|
{"level high", "MINIMAX-M2", []byte(`{}`), thinking.ThinkingConfig{Mode: thinking.ModeLevel, Level: thinking.LevelHigh}, true, "", false},
|
|
{"level low", "minimax-m2.1", []byte(`{}`), thinking.ThinkingConfig{Mode: thinking.ModeLevel, Level: thinking.LevelLow}, true, "", false},
|
|
{"level minimal", "minimax-m2", []byte(`{}`), thinking.ThinkingConfig{Mode: thinking.ModeLevel, Level: thinking.LevelMinimal}, true, "", false},
|
|
{"level medium", "minimax-m2", []byte(`{}`), thinking.ThinkingConfig{Mode: thinking.ModeLevel, Level: thinking.LevelMedium}, true, "", false},
|
|
{"level xhigh", "minimax-m2", []byte(`{}`), thinking.ThinkingConfig{Mode: thinking.ModeLevel, Level: thinking.LevelXHigh}, true, "", false},
|
|
{"budget zero", "minimax-m2", []byte(`{}`), thinking.ThinkingConfig{Mode: thinking.ModeBudget, Budget: 0}, false, "", false},
|
|
{"budget 1000", "minimax-m2.1", []byte(`{}`), thinking.ThinkingConfig{Mode: thinking.ModeBudget, Budget: 1000}, true, "", false},
|
|
{"unknown level", "minimax-m2", []byte(`{}`), thinking.ThinkingConfig{Mode: thinking.ModeLevel, Level: "unknown"}, true, "", false},
|
|
{"preserve fields", "minimax-m2", []byte(`{"model":"minimax-m2","extra":{"keep":true}}`), thinking.ThinkingConfig{Mode: thinking.ModeAuto}, true, "minimax-m2", true},
|
|
{"empty body", "minimax-m2", nil, thinking.ThinkingConfig{Mode: thinking.ModeAuto}, true, "", false},
|
|
{"malformed json", "minimax-m2", []byte(`{invalid`), thinking.ThinkingConfig{Mode: thinking.ModeAuto}, true, "", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
modelInfo := ®istry.ModelInfo{
|
|
ID: tt.modelID,
|
|
Thinking: ®istry.ThinkingSupport{},
|
|
}
|
|
got, err := applier.Apply(tt.body, tt.config, modelInfo)
|
|
if err != nil {
|
|
t.Fatalf("Apply() error = %v", err)
|
|
}
|
|
if !gjson.ValidBytes(got) {
|
|
t.Fatalf("expected valid JSON, got %s", string(got))
|
|
}
|
|
|
|
splitResult := gjson.GetBytes(got, "reasoning_split")
|
|
if !splitResult.Exists() {
|
|
t.Fatalf("reasoning_split missing")
|
|
}
|
|
// Verify JSON type is boolean, not string
|
|
if splitResult.Type != gjson.True && splitResult.Type != gjson.False {
|
|
t.Fatalf("reasoning_split should be boolean, got type %v", splitResult.Type)
|
|
}
|
|
gotSplit := splitResult.Bool()
|
|
if gotSplit != tt.wantSplit {
|
|
t.Fatalf("reasoning_split = %v, want %v", gotSplit, tt.wantSplit)
|
|
}
|
|
|
|
if tt.wantModel != "" {
|
|
gotModel := gjson.GetBytes(got, "model").String()
|
|
if gotModel != tt.wantModel {
|
|
t.Fatalf("model = %q, want %q", gotModel, tt.wantModel)
|
|
}
|
|
if tt.wantKeep && !gjson.GetBytes(got, "extra.keep").Bool() {
|
|
t.Fatalf("expected extra.keep preserved")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestIsGLMModel tests the GLM model detection.
|
|
//
|
|
// Depends on: Epic 9 Story 9-1
|
|
func TestIsGLMModel(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
model string
|
|
wantGLM bool
|
|
}{
|
|
{"glm-4.6", "glm-4.6", true},
|
|
{"glm-z1-preview", "glm-z1-preview", true},
|
|
{"glm uppercase", "GLM-4.7", true},
|
|
{"minimax-01", "minimax-01", false},
|
|
{"gpt-5.2", "gpt-5.2", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := isGLMModel(tt.model); got != tt.wantGLM {
|
|
t.Fatalf("isGLMModel(%q) = %v, want %v", tt.model, got, tt.wantGLM)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestIsMiniMaxModel tests the MiniMax model detection.
|
|
//
|
|
// Depends on: Epic 9 Story 9-1
|
|
func TestIsMiniMaxModel(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
model string
|
|
wantMiniMax bool
|
|
}{
|
|
{"minimax-01", "minimax-01", true},
|
|
{"minimax uppercase", "MINIMAX-M2", true},
|
|
{"glm-4.6", "glm-4.6", false},
|
|
{"gpt-5.2", "gpt-5.2", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := isMiniMaxModel(tt.model); got != tt.wantMiniMax {
|
|
t.Fatalf("isMiniMaxModel(%q) = %v, want %v", tt.model, got, tt.wantMiniMax)
|
|
}
|
|
})
|
|
}
|
|
}
|