mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-02 20:40:52 +08:00
feat(thinking): add numeric budget to thinkingLevel conversion fallback
This commit is contained in:
@@ -288,49 +288,73 @@ func ApplyDefaultThinkingIfNeeded(model string, body []byte) []byte {
|
|||||||
|
|
||||||
// ApplyGemini3ThinkingLevelFromMetadata applies thinkingLevel from metadata for Gemini 3 models.
|
// ApplyGemini3ThinkingLevelFromMetadata applies thinkingLevel from metadata for Gemini 3 models.
|
||||||
// For standard Gemini API format (generationConfig.thinkingConfig path).
|
// For standard Gemini API format (generationConfig.thinkingConfig path).
|
||||||
// This handles the case where reasoning_effort is specified via model name suffix (e.g., model(minimal)).
|
// This handles the case where reasoning_effort is specified via model name suffix (e.g., model(minimal))
|
||||||
|
// or numeric budget suffix (e.g., model(1000)) which gets converted to a thinkingLevel.
|
||||||
func ApplyGemini3ThinkingLevelFromMetadata(model string, metadata map[string]any, body []byte) []byte {
|
func ApplyGemini3ThinkingLevelFromMetadata(model string, metadata map[string]any, body []byte) []byte {
|
||||||
// Use the alias from metadata if available for model type detection
|
// Use the alias from metadata if available for model type detection
|
||||||
lookupModel := ResolveOriginalModel(model, metadata)
|
lookupModel := ResolveOriginalModel(model, metadata)
|
||||||
if !IsGemini3Model(lookupModel) && !IsGemini3Model(model) {
|
if !IsGemini3Model(lookupModel) && !IsGemini3Model(model) {
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
effort, ok := ReasoningEffortFromMetadata(metadata)
|
|
||||||
if !ok || effort == "" {
|
// Determine which model to use for validation
|
||||||
return body
|
|
||||||
}
|
|
||||||
// Validate and apply the thinkingLevel using the model that matches Gemini 3 pattern
|
|
||||||
checkModel := model
|
checkModel := model
|
||||||
if IsGemini3Model(lookupModel) {
|
if IsGemini3Model(lookupModel) {
|
||||||
checkModel = lookupModel
|
checkModel = lookupModel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// First try to get effort string from metadata
|
||||||
|
effort, ok := ReasoningEffortFromMetadata(metadata)
|
||||||
|
if ok && effort != "" {
|
||||||
if level, valid := ValidateGemini3ThinkingLevel(checkModel, effort); valid {
|
if level, valid := ValidateGemini3ThinkingLevel(checkModel, effort); valid {
|
||||||
return ApplyGeminiThinkingLevel(body, level, nil)
|
return ApplyGeminiThinkingLevel(body, level, nil)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: check for numeric budget and convert to thinkingLevel
|
||||||
|
budget, _, _, matched := ThinkingFromMetadata(metadata)
|
||||||
|
if matched && budget != nil {
|
||||||
|
if level, valid := ThinkingBudgetToGemini3Level(checkModel, *budget); valid {
|
||||||
|
return ApplyGeminiThinkingLevel(body, level, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApplyGemini3ThinkingLevelFromMetadataCLI applies thinkingLevel from metadata for Gemini 3 models.
|
// ApplyGemini3ThinkingLevelFromMetadataCLI applies thinkingLevel from metadata for Gemini 3 models.
|
||||||
// For Gemini CLI API format (request.generationConfig.thinkingConfig path).
|
// For Gemini CLI API format (request.generationConfig.thinkingConfig path).
|
||||||
// This handles the case where reasoning_effort is specified via model name suffix (e.g., model(minimal)).
|
// This handles the case where reasoning_effort is specified via model name suffix (e.g., model(minimal))
|
||||||
|
// or numeric budget suffix (e.g., model(1000)) which gets converted to a thinkingLevel.
|
||||||
func ApplyGemini3ThinkingLevelFromMetadataCLI(model string, metadata map[string]any, body []byte) []byte {
|
func ApplyGemini3ThinkingLevelFromMetadataCLI(model string, metadata map[string]any, body []byte) []byte {
|
||||||
// Use the alias from metadata if available for model type detection
|
// Use the alias from metadata if available for model type detection
|
||||||
lookupModel := ResolveOriginalModel(model, metadata)
|
lookupModel := ResolveOriginalModel(model, metadata)
|
||||||
if !IsGemini3Model(lookupModel) && !IsGemini3Model(model) {
|
if !IsGemini3Model(lookupModel) && !IsGemini3Model(model) {
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
effort, ok := ReasoningEffortFromMetadata(metadata)
|
|
||||||
if !ok || effort == "" {
|
// Determine which model to use for validation
|
||||||
return body
|
|
||||||
}
|
|
||||||
// Validate and apply the thinkingLevel using the model that matches Gemini 3 pattern
|
|
||||||
checkModel := model
|
checkModel := model
|
||||||
if IsGemini3Model(lookupModel) {
|
if IsGemini3Model(lookupModel) {
|
||||||
checkModel = lookupModel
|
checkModel = lookupModel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// First try to get effort string from metadata
|
||||||
|
effort, ok := ReasoningEffortFromMetadata(metadata)
|
||||||
|
if ok && effort != "" {
|
||||||
if level, valid := ValidateGemini3ThinkingLevel(checkModel, effort); valid {
|
if level, valid := ValidateGemini3ThinkingLevel(checkModel, effort); valid {
|
||||||
return ApplyGeminiCLIThinkingLevel(body, level, nil)
|
return ApplyGeminiCLIThinkingLevel(body, level, nil)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: check for numeric budget and convert to thinkingLevel
|
||||||
|
budget, _, _, matched := ThinkingFromMetadata(metadata)
|
||||||
|
if matched && budget != nil {
|
||||||
|
if level, valid := ThinkingBudgetToGemini3Level(checkModel, *budget); valid {
|
||||||
|
return ApplyGeminiCLIThinkingLevel(body, level, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
193
test/model_alias_thinking_suffix_test.go
Normal file
193
test/model_alias_thinking_suffix_test.go
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"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
|
||||||
|
baseModel, 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: For aliases, simulate the model mapping by adding upstream model info
|
||||||
|
if tt.isAlias {
|
||||||
|
if metadata == nil {
|
||||||
|
metadata = make(map[string]any)
|
||||||
|
}
|
||||||
|
metadata[util.ModelMappingOriginalModelMetadataKey] = baseModel
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 for the function
|
||||||
|
testMetadata := make(map[string]any)
|
||||||
|
if tt.isAlias {
|
||||||
|
testMetadata[util.ModelMappingOriginalModelMetadataKey] = tt.upstreamModel
|
||||||
|
}
|
||||||
|
// Copy parsed metadata
|
||||||
|
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
|
||||||
|
if tt.expectedField == "thinkingBudget" && util.IsGemini25Model(tt.upstreamModel) {
|
||||||
|
budget, _, _, _ := util.ThinkingFromMetadata(metadata)
|
||||||
|
if budget != nil {
|
||||||
|
body := []byte(`{"request":{"contents":[]}}`)
|
||||||
|
result := util.ApplyGeminiCLIThinkingConfig(body, budget, nil)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user