Spaces:
Paused
Paused
hkfires
fix(thinking): fallback to upstream model for thinking support when alias not in registry
c17819a | package test | |
| import ( | |
| "testing" | |
| "github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor" | |
| "github.com/router-for-me/CLIProxyAPI/v6/internal/util" | |
| "github.com/tidwall/gjson" | |
| ) | |
| // 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 real ApplyThinkingMetadataCLI flow | |
| 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 | |
| } | |
| // Use the exported ApplyThinkingMetadataCLI which includes the fallback logic | |
| result := executor.ApplyThinkingMetadataCLI(body, testMetadata, tt.upstreamModel) | |
| 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) | |
| } | |
| } | |
| }) | |
| } | |
| } | |