mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-02 04:20:50 +08:00
test(thinking): remove legacy unit and integration tests
This commit is contained in:
@@ -1,423 +0,0 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// registerGemini3Models loads Gemini 3 models into the registry for testing.
|
||||
func registerGemini3Models(t *testing.T) func() {
|
||||
t.Helper()
|
||||
reg := registry.GetGlobalRegistry()
|
||||
uid := fmt.Sprintf("gemini3-test-%d", time.Now().UnixNano())
|
||||
reg.RegisterClient(uid+"-gemini", "gemini", registry.GetGeminiModels())
|
||||
reg.RegisterClient(uid+"-aistudio", "aistudio", registry.GetAIStudioModels())
|
||||
return func() {
|
||||
reg.UnregisterClient(uid + "-gemini")
|
||||
reg.UnregisterClient(uid + "-aistudio")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsGemini3Model(t *testing.T) {
|
||||
cases := []struct {
|
||||
model string
|
||||
expected bool
|
||||
}{
|
||||
{"gemini-3-pro-preview", true},
|
||||
{"gemini-3-flash-preview", true},
|
||||
{"gemini_3_pro_preview", true},
|
||||
{"gemini-3-pro", true},
|
||||
{"gemini-3-flash", true},
|
||||
{"GEMINI-3-PRO-PREVIEW", true},
|
||||
{"gemini-2.5-pro", false},
|
||||
{"gemini-2.5-flash", false},
|
||||
{"gpt-5", false},
|
||||
{"claude-sonnet-4-5", false},
|
||||
{"", false},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.model, func(t *testing.T) {
|
||||
got := util.IsGemini3Model(cs.model)
|
||||
if got != cs.expected {
|
||||
t.Fatalf("IsGemini3Model(%q) = %v, want %v", cs.model, got, cs.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsGemini3ProModel(t *testing.T) {
|
||||
cases := []struct {
|
||||
model string
|
||||
expected bool
|
||||
}{
|
||||
{"gemini-3-pro-preview", true},
|
||||
{"gemini_3_pro_preview", true},
|
||||
{"gemini-3-pro", true},
|
||||
{"GEMINI-3-PRO-PREVIEW", true},
|
||||
{"gemini-3-flash-preview", false},
|
||||
{"gemini-3-flash", false},
|
||||
{"gemini-2.5-pro", false},
|
||||
{"", false},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.model, func(t *testing.T) {
|
||||
got := util.IsGemini3ProModel(cs.model)
|
||||
if got != cs.expected {
|
||||
t.Fatalf("IsGemini3ProModel(%q) = %v, want %v", cs.model, got, cs.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsGemini3FlashModel(t *testing.T) {
|
||||
cases := []struct {
|
||||
model string
|
||||
expected bool
|
||||
}{
|
||||
{"gemini-3-flash-preview", true},
|
||||
{"gemini_3_flash_preview", true},
|
||||
{"gemini-3-flash", true},
|
||||
{"GEMINI-3-FLASH-PREVIEW", true},
|
||||
{"gemini-3-pro-preview", false},
|
||||
{"gemini-3-pro", false},
|
||||
{"gemini-2.5-flash", false},
|
||||
{"", false},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.model, func(t *testing.T) {
|
||||
got := util.IsGemini3FlashModel(cs.model)
|
||||
if got != cs.expected {
|
||||
t.Fatalf("IsGemini3FlashModel(%q) = %v, want %v", cs.model, got, cs.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateGemini3ThinkingLevel(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
model string
|
||||
level string
|
||||
wantOK bool
|
||||
wantVal string
|
||||
}{
|
||||
// Gemini 3 Pro: supports "low", "high"
|
||||
{"pro-low", "gemini-3-pro-preview", "low", true, "low"},
|
||||
{"pro-high", "gemini-3-pro-preview", "high", true, "high"},
|
||||
{"pro-minimal-invalid", "gemini-3-pro-preview", "minimal", false, ""},
|
||||
{"pro-medium-invalid", "gemini-3-pro-preview", "medium", false, ""},
|
||||
|
||||
// Gemini 3 Flash: supports "minimal", "low", "medium", "high"
|
||||
{"flash-minimal", "gemini-3-flash-preview", "minimal", true, "minimal"},
|
||||
{"flash-low", "gemini-3-flash-preview", "low", true, "low"},
|
||||
{"flash-medium", "gemini-3-flash-preview", "medium", true, "medium"},
|
||||
{"flash-high", "gemini-3-flash-preview", "high", true, "high"},
|
||||
|
||||
// Case insensitivity
|
||||
{"flash-LOW-case", "gemini-3-flash-preview", "LOW", true, "low"},
|
||||
{"flash-High-case", "gemini-3-flash-preview", "High", true, "high"},
|
||||
{"pro-HIGH-case", "gemini-3-pro-preview", "HIGH", true, "high"},
|
||||
|
||||
// Invalid levels
|
||||
{"flash-invalid", "gemini-3-flash-preview", "xhigh", false, ""},
|
||||
{"flash-invalid-auto", "gemini-3-flash-preview", "auto", false, ""},
|
||||
{"flash-empty", "gemini-3-flash-preview", "", false, ""},
|
||||
|
||||
// Non-Gemini 3 models
|
||||
{"non-gemini3", "gemini-2.5-pro", "high", false, ""},
|
||||
{"gpt5", "gpt-5", "high", false, ""},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.name, func(t *testing.T) {
|
||||
got, ok := util.ValidateGemini3ThinkingLevel(cs.model, cs.level)
|
||||
if ok != cs.wantOK {
|
||||
t.Fatalf("ValidateGemini3ThinkingLevel(%q, %q) ok = %v, want %v", cs.model, cs.level, ok, cs.wantOK)
|
||||
}
|
||||
if got != cs.wantVal {
|
||||
t.Fatalf("ValidateGemini3ThinkingLevel(%q, %q) = %q, want %q", cs.model, cs.level, got, cs.wantVal)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestThinkingBudgetToGemini3Level(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
model string
|
||||
budget int
|
||||
wantOK bool
|
||||
wantVal string
|
||||
}{
|
||||
// Gemini 3 Pro: maps to "low" or "high"
|
||||
{"pro-dynamic", "gemini-3-pro-preview", -1, true, "high"},
|
||||
{"pro-zero", "gemini-3-pro-preview", 0, true, "low"},
|
||||
{"pro-small", "gemini-3-pro-preview", 1000, true, "low"},
|
||||
{"pro-medium", "gemini-3-pro-preview", 8000, true, "low"},
|
||||
{"pro-large", "gemini-3-pro-preview", 20000, true, "high"},
|
||||
{"pro-huge", "gemini-3-pro-preview", 50000, true, "high"},
|
||||
|
||||
// Gemini 3 Flash: maps to "minimal", "low", "medium", "high"
|
||||
{"flash-dynamic", "gemini-3-flash-preview", -1, true, "high"},
|
||||
{"flash-zero", "gemini-3-flash-preview", 0, true, "minimal"},
|
||||
{"flash-tiny", "gemini-3-flash-preview", 500, true, "minimal"},
|
||||
{"flash-small", "gemini-3-flash-preview", 1000, true, "low"},
|
||||
{"flash-medium-val", "gemini-3-flash-preview", 8000, true, "medium"},
|
||||
{"flash-large", "gemini-3-flash-preview", 20000, true, "high"},
|
||||
{"flash-huge", "gemini-3-flash-preview", 50000, true, "high"},
|
||||
|
||||
// Non-Gemini 3 models should return false
|
||||
{"gemini25-budget", "gemini-2.5-pro", 8000, false, ""},
|
||||
{"gpt5-budget", "gpt-5", 8000, false, ""},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.name, func(t *testing.T) {
|
||||
got, ok := util.ThinkingBudgetToGemini3Level(cs.model, cs.budget)
|
||||
if ok != cs.wantOK {
|
||||
t.Fatalf("ThinkingBudgetToGemini3Level(%q, %d) ok = %v, want %v", cs.model, cs.budget, ok, cs.wantOK)
|
||||
}
|
||||
if got != cs.wantVal {
|
||||
t.Fatalf("ThinkingBudgetToGemini3Level(%q, %d) = %q, want %q", cs.model, cs.budget, got, cs.wantVal)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyGemini3ThinkingLevelFromMetadata(t *testing.T) {
|
||||
cleanup := registerGemini3Models(t)
|
||||
defer cleanup()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
model string
|
||||
metadata map[string]any
|
||||
inputBody string
|
||||
wantLevel string
|
||||
wantInclude bool
|
||||
wantNoChange bool
|
||||
}{
|
||||
{
|
||||
name: "flash-minimal-from-suffix",
|
||||
model: "gemini-3-flash-preview",
|
||||
metadata: map[string]any{"reasoning_effort": "minimal"},
|
||||
inputBody: `{"generationConfig":{"thinkingConfig":{"includeThoughts":true}}}`,
|
||||
wantLevel: "minimal",
|
||||
wantInclude: true,
|
||||
},
|
||||
{
|
||||
name: "flash-medium-from-suffix",
|
||||
model: "gemini-3-flash-preview",
|
||||
metadata: map[string]any{"reasoning_effort": "medium"},
|
||||
inputBody: `{"generationConfig":{"thinkingConfig":{"includeThoughts":true}}}`,
|
||||
wantLevel: "medium",
|
||||
wantInclude: true,
|
||||
},
|
||||
{
|
||||
name: "pro-high-from-suffix",
|
||||
model: "gemini-3-pro-preview",
|
||||
metadata: map[string]any{"reasoning_effort": "high"},
|
||||
inputBody: `{"generationConfig":{"thinkingConfig":{"includeThoughts":true}}}`,
|
||||
wantLevel: "high",
|
||||
wantInclude: true,
|
||||
},
|
||||
{
|
||||
name: "no-metadata-no-change",
|
||||
model: "gemini-3-flash-preview",
|
||||
metadata: nil,
|
||||
inputBody: `{"generationConfig":{"thinkingConfig":{"includeThoughts":true}}}`,
|
||||
wantNoChange: true,
|
||||
},
|
||||
{
|
||||
name: "non-gemini3-no-change",
|
||||
model: "gemini-2.5-pro",
|
||||
metadata: map[string]any{"reasoning_effort": "high"},
|
||||
inputBody: `{"generationConfig":{"thinkingConfig":{"thinkingBudget":-1}}}`,
|
||||
wantNoChange: true,
|
||||
},
|
||||
{
|
||||
name: "invalid-level-no-change",
|
||||
model: "gemini-3-flash-preview",
|
||||
metadata: map[string]any{"reasoning_effort": "xhigh"},
|
||||
inputBody: `{"generationConfig":{"thinkingConfig":{"includeThoughts":true}}}`,
|
||||
wantNoChange: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.name, func(t *testing.T) {
|
||||
input := []byte(cs.inputBody)
|
||||
result := util.ApplyGemini3ThinkingLevelFromMetadata(cs.model, cs.metadata, input)
|
||||
|
||||
if cs.wantNoChange {
|
||||
if string(result) != cs.inputBody {
|
||||
t.Fatalf("expected no change, but got: %s", string(result))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
level := gjson.GetBytes(result, "generationConfig.thinkingConfig.thinkingLevel")
|
||||
if !level.Exists() {
|
||||
t.Fatalf("thinkingLevel not set in result: %s", string(result))
|
||||
}
|
||||
if level.String() != cs.wantLevel {
|
||||
t.Fatalf("thinkingLevel = %q, want %q", level.String(), cs.wantLevel)
|
||||
}
|
||||
|
||||
include := gjson.GetBytes(result, "generationConfig.thinkingConfig.includeThoughts")
|
||||
if cs.wantInclude && (!include.Exists() || !include.Bool()) {
|
||||
t.Fatalf("includeThoughts should be true, got: %s", string(result))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyGemini3ThinkingLevelFromMetadataCLI(t *testing.T) {
|
||||
cleanup := registerGemini3Models(t)
|
||||
defer cleanup()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
model string
|
||||
metadata map[string]any
|
||||
inputBody string
|
||||
wantLevel string
|
||||
wantInclude bool
|
||||
wantNoChange bool
|
||||
}{
|
||||
{
|
||||
name: "flash-minimal-from-suffix-cli",
|
||||
model: "gemini-3-flash-preview",
|
||||
metadata: map[string]any{"reasoning_effort": "minimal"},
|
||||
inputBody: `{"request":{"generationConfig":{"thinkingConfig":{"includeThoughts":true}}}}`,
|
||||
wantLevel: "minimal",
|
||||
wantInclude: true,
|
||||
},
|
||||
{
|
||||
name: "flash-low-from-suffix-cli",
|
||||
model: "gemini-3-flash-preview",
|
||||
metadata: map[string]any{"reasoning_effort": "low"},
|
||||
inputBody: `{"request":{"generationConfig":{"thinkingConfig":{"includeThoughts":true}}}}`,
|
||||
wantLevel: "low",
|
||||
wantInclude: true,
|
||||
},
|
||||
{
|
||||
name: "pro-low-from-suffix-cli",
|
||||
model: "gemini-3-pro-preview",
|
||||
metadata: map[string]any{"reasoning_effort": "low"},
|
||||
inputBody: `{"request":{"generationConfig":{"thinkingConfig":{"includeThoughts":true}}}}`,
|
||||
wantLevel: "low",
|
||||
wantInclude: true,
|
||||
},
|
||||
{
|
||||
name: "no-metadata-no-change-cli",
|
||||
model: "gemini-3-flash-preview",
|
||||
metadata: nil,
|
||||
inputBody: `{"request":{"generationConfig":{"thinkingConfig":{"includeThoughts":true}}}}`,
|
||||
wantNoChange: true,
|
||||
},
|
||||
{
|
||||
name: "non-gemini3-no-change-cli",
|
||||
model: "gemini-2.5-pro",
|
||||
metadata: map[string]any{"reasoning_effort": "high"},
|
||||
inputBody: `{"request":{"generationConfig":{"thinkingConfig":{"thinkingBudget":-1}}}}`,
|
||||
wantNoChange: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.name, func(t *testing.T) {
|
||||
input := []byte(cs.inputBody)
|
||||
result := util.ApplyGemini3ThinkingLevelFromMetadataCLI(cs.model, cs.metadata, input)
|
||||
|
||||
if cs.wantNoChange {
|
||||
if string(result) != cs.inputBody {
|
||||
t.Fatalf("expected no change, but got: %s", string(result))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
level := gjson.GetBytes(result, "request.generationConfig.thinkingConfig.thinkingLevel")
|
||||
if !level.Exists() {
|
||||
t.Fatalf("thinkingLevel not set in result: %s", string(result))
|
||||
}
|
||||
if level.String() != cs.wantLevel {
|
||||
t.Fatalf("thinkingLevel = %q, want %q", level.String(), cs.wantLevel)
|
||||
}
|
||||
|
||||
include := gjson.GetBytes(result, "request.generationConfig.thinkingConfig.includeThoughts")
|
||||
if cs.wantInclude && (!include.Exists() || !include.Bool()) {
|
||||
t.Fatalf("includeThoughts should be true, got: %s", string(result))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeGeminiThinkingBudget_Gemini3Conversion(t *testing.T) {
|
||||
cleanup := registerGemini3Models(t)
|
||||
defer cleanup()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
model string
|
||||
inputBody string
|
||||
wantLevel string
|
||||
wantBudget bool // if true, expect thinkingBudget instead of thinkingLevel
|
||||
}{
|
||||
{
|
||||
name: "gemini3-flash-budget-to-level",
|
||||
model: "gemini-3-flash-preview",
|
||||
inputBody: `{"generationConfig":{"thinkingConfig":{"thinkingBudget":8000}}}`,
|
||||
wantLevel: "medium",
|
||||
},
|
||||
{
|
||||
name: "gemini3-pro-budget-to-level",
|
||||
model: "gemini-3-pro-preview",
|
||||
inputBody: `{"generationConfig":{"thinkingConfig":{"thinkingBudget":20000}}}`,
|
||||
wantLevel: "high",
|
||||
},
|
||||
{
|
||||
name: "gemini25-keeps-budget",
|
||||
model: "gemini-2.5-pro",
|
||||
inputBody: `{"generationConfig":{"thinkingConfig":{"thinkingBudget":8000}}}`,
|
||||
wantBudget: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.name, func(t *testing.T) {
|
||||
result := util.NormalizeGeminiThinkingBudget(cs.model, []byte(cs.inputBody))
|
||||
|
||||
if cs.wantBudget {
|
||||
budget := gjson.GetBytes(result, "generationConfig.thinkingConfig.thinkingBudget")
|
||||
if !budget.Exists() {
|
||||
t.Fatalf("thinkingBudget should exist for non-Gemini3 model: %s", string(result))
|
||||
}
|
||||
level := gjson.GetBytes(result, "generationConfig.thinkingConfig.thinkingLevel")
|
||||
if level.Exists() {
|
||||
t.Fatalf("thinkingLevel should not exist for non-Gemini3 model: %s", string(result))
|
||||
}
|
||||
} else {
|
||||
level := gjson.GetBytes(result, "generationConfig.thinkingConfig.thinkingLevel")
|
||||
if !level.Exists() {
|
||||
t.Fatalf("thinkingLevel should exist for Gemini3 model: %s", string(result))
|
||||
}
|
||||
if level.String() != cs.wantLevel {
|
||||
t.Fatalf("thinkingLevel = %q, want %q", level.String(), cs.wantLevel)
|
||||
}
|
||||
budget := gjson.GetBytes(result, "generationConfig.thinkingConfig.thinkingBudget")
|
||||
if budget.Exists() {
|
||||
t.Fatalf("thinkingBudget should be removed for Gemini3 model: %s", string(result))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,262 +0,0 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
// TestModelAliasThinkingSuffix tests the 32 test cases defined in docs/thinking_suffix_test_cases.md
|
||||
// These tests verify the thinking suffix parsing and application logic across different providers.
|
||||
func TestModelAliasThinkingSuffix(t *testing.T) {
|
||||
tests := []struct {
|
||||
id int
|
||||
name string
|
||||
provider string
|
||||
requestModel string
|
||||
suffixType string
|
||||
expectedField string // "thinkingBudget", "thinkingLevel", "budget_tokens", "reasoning_effort", "enable_thinking"
|
||||
expectedValue any
|
||||
upstreamModel string // The upstream model after alias resolution
|
||||
isAlias bool
|
||||
}{
|
||||
// === 1. Antigravity Provider ===
|
||||
// 1.1 Budget-only models (Gemini 2.5)
|
||||
{1, "antigravity_original_numeric", "antigravity", "gemini-2.5-computer-use-preview-10-2025(1000)", "numeric", "thinkingBudget", 1000, "gemini-2.5-computer-use-preview-10-2025", false},
|
||||
{2, "antigravity_alias_numeric", "antigravity", "gp(1000)", "numeric", "thinkingBudget", 1000, "gemini-2.5-computer-use-preview-10-2025", true},
|
||||
// 1.2 Budget+Levels models (Gemini 3)
|
||||
{3, "antigravity_original_numeric_to_level", "antigravity", "gemini-3-flash-preview(1000)", "numeric", "thinkingLevel", "low", "gemini-3-flash-preview", false},
|
||||
{4, "antigravity_original_level", "antigravity", "gemini-3-flash-preview(low)", "level", "thinkingLevel", "low", "gemini-3-flash-preview", false},
|
||||
{5, "antigravity_alias_numeric_to_level", "antigravity", "gf(1000)", "numeric", "thinkingLevel", "low", "gemini-3-flash-preview", true},
|
||||
{6, "antigravity_alias_level", "antigravity", "gf(low)", "level", "thinkingLevel", "low", "gemini-3-flash-preview", true},
|
||||
|
||||
// === 2. Gemini CLI Provider ===
|
||||
// 2.1 Budget-only models
|
||||
{7, "gemini_cli_original_numeric", "gemini-cli", "gemini-2.5-pro(8192)", "numeric", "thinkingBudget", 8192, "gemini-2.5-pro", false},
|
||||
{8, "gemini_cli_alias_numeric", "gemini-cli", "g25p(8192)", "numeric", "thinkingBudget", 8192, "gemini-2.5-pro", true},
|
||||
// 2.2 Budget+Levels models
|
||||
{9, "gemini_cli_original_numeric_to_level", "gemini-cli", "gemini-3-flash-preview(1000)", "numeric", "thinkingLevel", "low", "gemini-3-flash-preview", false},
|
||||
{10, "gemini_cli_original_level", "gemini-cli", "gemini-3-flash-preview(low)", "level", "thinkingLevel", "low", "gemini-3-flash-preview", false},
|
||||
{11, "gemini_cli_alias_numeric_to_level", "gemini-cli", "gf(1000)", "numeric", "thinkingLevel", "low", "gemini-3-flash-preview", true},
|
||||
{12, "gemini_cli_alias_level", "gemini-cli", "gf(low)", "level", "thinkingLevel", "low", "gemini-3-flash-preview", true},
|
||||
|
||||
// === 3. Vertex Provider ===
|
||||
// 3.1 Budget-only models
|
||||
{13, "vertex_original_numeric", "vertex", "gemini-2.5-pro(16384)", "numeric", "thinkingBudget", 16384, "gemini-2.5-pro", false},
|
||||
{14, "vertex_alias_numeric", "vertex", "vg25p(16384)", "numeric", "thinkingBudget", 16384, "gemini-2.5-pro", true},
|
||||
// 3.2 Budget+Levels models
|
||||
{15, "vertex_original_numeric_to_level", "vertex", "gemini-3-flash-preview(1000)", "numeric", "thinkingLevel", "low", "gemini-3-flash-preview", false},
|
||||
{16, "vertex_original_level", "vertex", "gemini-3-flash-preview(low)", "level", "thinkingLevel", "low", "gemini-3-flash-preview", false},
|
||||
{17, "vertex_alias_numeric_to_level", "vertex", "vgf(1000)", "numeric", "thinkingLevel", "low", "gemini-3-flash-preview", true},
|
||||
{18, "vertex_alias_level", "vertex", "vgf(low)", "level", "thinkingLevel", "low", "gemini-3-flash-preview", true},
|
||||
|
||||
// === 4. AI Studio Provider ===
|
||||
// 4.1 Budget-only models
|
||||
{19, "aistudio_original_numeric", "aistudio", "gemini-2.5-pro(12000)", "numeric", "thinkingBudget", 12000, "gemini-2.5-pro", false},
|
||||
{20, "aistudio_alias_numeric", "aistudio", "ag25p(12000)", "numeric", "thinkingBudget", 12000, "gemini-2.5-pro", true},
|
||||
// 4.2 Budget+Levels models
|
||||
{21, "aistudio_original_numeric_to_level", "aistudio", "gemini-3-flash-preview(1000)", "numeric", "thinkingLevel", "low", "gemini-3-flash-preview", false},
|
||||
{22, "aistudio_original_level", "aistudio", "gemini-3-flash-preview(low)", "level", "thinkingLevel", "low", "gemini-3-flash-preview", false},
|
||||
{23, "aistudio_alias_numeric_to_level", "aistudio", "agf(1000)", "numeric", "thinkingLevel", "low", "gemini-3-flash-preview", true},
|
||||
{24, "aistudio_alias_level", "aistudio", "agf(low)", "level", "thinkingLevel", "low", "gemini-3-flash-preview", true},
|
||||
|
||||
// === 5. Claude Provider ===
|
||||
{25, "claude_original_numeric", "claude", "claude-sonnet-4-5-20250929(16384)", "numeric", "budget_tokens", 16384, "claude-sonnet-4-5-20250929", false},
|
||||
{26, "claude_alias_numeric", "claude", "cs45(16384)", "numeric", "budget_tokens", 16384, "claude-sonnet-4-5-20250929", true},
|
||||
|
||||
// === 6. Codex Provider ===
|
||||
{27, "codex_original_level", "codex", "gpt-5(high)", "level", "reasoning_effort", "high", "gpt-5", false},
|
||||
{28, "codex_alias_level", "codex", "g5(high)", "level", "reasoning_effort", "high", "gpt-5", true},
|
||||
|
||||
// === 7. Qwen Provider ===
|
||||
{29, "qwen_original_level", "qwen", "qwen3-coder-plus(high)", "level", "enable_thinking", true, "qwen3-coder-plus", false},
|
||||
{30, "qwen_alias_level", "qwen", "qcp(high)", "level", "enable_thinking", true, "qwen3-coder-plus", true},
|
||||
|
||||
// === 8. iFlow Provider ===
|
||||
{31, "iflow_original_level", "iflow", "glm-4.7(high)", "level", "reasoning_effort", "high", "glm-4.7", false},
|
||||
{32, "iflow_alias_level", "iflow", "glm(high)", "level", "reasoning_effort", "high", "glm-4.7", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Step 1: Parse model suffix (simulates SDK layer normalization)
|
||||
// For "gp(1000)" -> requestedModel="gp", metadata={thinking_budget: 1000}
|
||||
requestedModel, metadata := util.NormalizeThinkingModel(tt.requestModel)
|
||||
|
||||
// Verify suffix was parsed
|
||||
if metadata == nil && (tt.suffixType == "numeric" || tt.suffixType == "level") {
|
||||
t.Errorf("Case #%d: NormalizeThinkingModel(%q) metadata is nil", tt.id, tt.requestModel)
|
||||
return
|
||||
}
|
||||
|
||||
// Step 2: Simulate OAuth model mapping
|
||||
// Real flow: applyOAuthModelMapping stores requestedModel (the alias) in metadata
|
||||
if tt.isAlias {
|
||||
if metadata == nil {
|
||||
metadata = make(map[string]any)
|
||||
}
|
||||
metadata[util.ModelMappingOriginalModelMetadataKey] = requestedModel
|
||||
}
|
||||
|
||||
// Step 3: Verify metadata extraction
|
||||
switch tt.suffixType {
|
||||
case "numeric":
|
||||
budget, _, _, matched := util.ThinkingFromMetadata(metadata)
|
||||
if !matched {
|
||||
t.Errorf("Case #%d: ThinkingFromMetadata did not match", tt.id)
|
||||
return
|
||||
}
|
||||
if budget == nil {
|
||||
t.Errorf("Case #%d: expected budget in metadata", tt.id)
|
||||
return
|
||||
}
|
||||
// For thinkingBudget/budget_tokens, verify the parsed budget value
|
||||
if tt.expectedField == "thinkingBudget" || tt.expectedField == "budget_tokens" {
|
||||
expectedBudget := tt.expectedValue.(int)
|
||||
if *budget != expectedBudget {
|
||||
t.Errorf("Case #%d: budget = %d, want %d", tt.id, *budget, expectedBudget)
|
||||
}
|
||||
}
|
||||
// For thinkingLevel (Gemini 3), verify conversion from budget to level
|
||||
if tt.expectedField == "thinkingLevel" {
|
||||
level, ok := util.ThinkingBudgetToGemini3Level(tt.upstreamModel, *budget)
|
||||
if !ok {
|
||||
t.Errorf("Case #%d: ThinkingBudgetToGemini3Level failed", tt.id)
|
||||
return
|
||||
}
|
||||
expectedLevel := tt.expectedValue.(string)
|
||||
if level != expectedLevel {
|
||||
t.Errorf("Case #%d: converted level = %q, want %q", tt.id, level, expectedLevel)
|
||||
}
|
||||
}
|
||||
|
||||
case "level":
|
||||
_, _, effort, matched := util.ThinkingFromMetadata(metadata)
|
||||
if !matched {
|
||||
t.Errorf("Case #%d: ThinkingFromMetadata did not match", tt.id)
|
||||
return
|
||||
}
|
||||
if effort == nil {
|
||||
t.Errorf("Case #%d: expected effort in metadata", tt.id)
|
||||
return
|
||||
}
|
||||
if tt.expectedField == "thinkingLevel" || tt.expectedField == "reasoning_effort" {
|
||||
expectedEffort := tt.expectedValue.(string)
|
||||
if *effort != expectedEffort {
|
||||
t.Errorf("Case #%d: effort = %q, want %q", tt.id, *effort, expectedEffort)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Test Gemini-specific thinkingLevel conversion for Gemini 3 models
|
||||
if tt.expectedField == "thinkingLevel" && util.IsGemini3Model(tt.upstreamModel) {
|
||||
body := []byte(`{"request":{"contents":[]}}`)
|
||||
|
||||
// Build metadata simulating real OAuth flow:
|
||||
// - requestedModel (alias like "gf") is stored in model_mapping_original_model
|
||||
// - upstreamModel is passed as the model parameter
|
||||
testMetadata := make(map[string]any)
|
||||
if tt.isAlias {
|
||||
// Real flow: applyOAuthModelMapping stores requestedModel (the alias)
|
||||
testMetadata[util.ModelMappingOriginalModelMetadataKey] = requestedModel
|
||||
}
|
||||
// Copy parsed metadata (thinking_budget, reasoning_effort, etc.)
|
||||
for k, v := range metadata {
|
||||
testMetadata[k] = v
|
||||
}
|
||||
|
||||
result := util.ApplyGemini3ThinkingLevelFromMetadataCLI(tt.upstreamModel, testMetadata, body)
|
||||
levelVal := gjson.GetBytes(result, "request.generationConfig.thinkingConfig.thinkingLevel")
|
||||
|
||||
expectedLevel := tt.expectedValue.(string)
|
||||
if !levelVal.Exists() {
|
||||
t.Errorf("Case #%d: expected thinkingLevel in result", tt.id)
|
||||
} else if levelVal.String() != expectedLevel {
|
||||
t.Errorf("Case #%d: thinkingLevel = %q, want %q", tt.id, levelVal.String(), expectedLevel)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: Test Gemini 2.5 thinkingBudget application using thinking.ApplyThinking
|
||||
if tt.expectedField == "thinkingBudget" && util.IsGemini25Model(tt.upstreamModel) {
|
||||
body := []byte(`{"request":{"contents":[]}}`)
|
||||
|
||||
// Build metadata simulating real OAuth flow:
|
||||
// - requestedModel (alias like "gp") is stored in model_mapping_original_model
|
||||
// - upstreamModel is passed as the model parameter
|
||||
testMetadata := make(map[string]any)
|
||||
if tt.isAlias {
|
||||
// Real flow: applyOAuthModelMapping stores requestedModel (the alias)
|
||||
testMetadata[util.ModelMappingOriginalModelMetadataKey] = requestedModel
|
||||
}
|
||||
// Copy parsed metadata (thinking_budget, reasoning_effort, etc.)
|
||||
for k, v := range metadata {
|
||||
testMetadata[k] = v
|
||||
}
|
||||
|
||||
// Merge thinking config from metadata into body
|
||||
body = applyThinkingFromMetadata(body, testMetadata)
|
||||
|
||||
// Use thinking.ApplyThinking for unified thinking config handling
|
||||
// Note: ApplyThinking now takes model string, not *ModelInfo
|
||||
result, _ := thinking.ApplyThinking(body, tt.upstreamModel, "gemini-cli")
|
||||
|
||||
budgetVal := gjson.GetBytes(result, "request.generationConfig.thinkingConfig.thinkingBudget")
|
||||
|
||||
expectedBudget := tt.expectedValue.(int)
|
||||
if !budgetVal.Exists() {
|
||||
t.Errorf("Case #%d: expected thinkingBudget in result", tt.id)
|
||||
} else if int(budgetVal.Int()) != expectedBudget {
|
||||
t.Errorf("Case #%d: thinkingBudget = %d, want %d", tt.id, int(budgetVal.Int()), expectedBudget)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// applyThinkingFromMetadata merges thinking configuration from metadata into the payload.
|
||||
func applyThinkingFromMetadata(payload []byte, metadata map[string]any) []byte {
|
||||
if len(metadata) == 0 {
|
||||
return payload
|
||||
}
|
||||
|
||||
// Merge thinking_budget from metadata if present
|
||||
if budget, ok := metadata["thinking_budget"]; ok {
|
||||
if budgetVal, okNum := parseNumberToInt(budget); okNum {
|
||||
payload, _ = sjson.SetBytes(payload, "request.generationConfig.thinkingConfig.thinkingBudget", budgetVal)
|
||||
}
|
||||
}
|
||||
|
||||
// Merge reasoning_effort from metadata if present
|
||||
if effort, ok := metadata["reasoning_effort"]; ok {
|
||||
if effortStr, okStr := effort.(string); okStr && effortStr != "" {
|
||||
payload, _ = sjson.SetBytes(payload, "request.generationConfig.thinkingConfig.thinkingLevel", effortStr)
|
||||
}
|
||||
}
|
||||
|
||||
// Merge thinking_include_thoughts from metadata if present
|
||||
if include, ok := metadata["thinking_include_thoughts"]; ok {
|
||||
if includeBool, okBool := include.(bool); okBool {
|
||||
payload, _ = sjson.SetBytes(payload, "request.generationConfig.thinkingConfig.includeThoughts", includeBool)
|
||||
}
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
// parseNumberToInt safely converts various numeric types to int
|
||||
func parseNumberToInt(raw any) (int, bool) {
|
||||
switch v := raw.(type) {
|
||||
case int:
|
||||
return v, true
|
||||
case int32:
|
||||
return int(v), true
|
||||
case int64:
|
||||
return int(v), true
|
||||
case float64:
|
||||
return int(v), true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
Reference in New Issue
Block a user