diff --git a/internal/logging/global_logger.go b/internal/logging/global_logger.go index 146f6c80..28c9f3b9 100644 --- a/internal/logging/global_logger.go +++ b/internal/logging/global_logger.go @@ -30,7 +30,7 @@ var ( type LogFormatter struct{} // logFieldOrder defines the display order for common log fields. -var logFieldOrder = []string{"provider", "model", "mode", "budget", "level", "original_value", "original_level", "min", "max", "clamped_to", "error"} +var logFieldOrder = []string{"provider", "model", "mode", "budget", "level", "original_mode", "original_value", "min", "max", "clamped_to", "error"} // Format renders a single log entry with custom formatting. func (m *LogFormatter) Format(entry *log.Entry) ([]byte, error) { diff --git a/internal/thinking/apply.go b/internal/thinking/apply.go index fe7d59b4..cf0e373b 100644 --- a/internal/thinking/apply.go +++ b/internal/thinking/apply.go @@ -159,7 +159,7 @@ func ApplyThinking(body []byte, model string, fromFormat string, toFormat string } // 5. Validate and normalize configuration - validated, err := ValidateConfig(config, modelInfo, fromFormat, providerFormat) + validated, err := ValidateConfig(config, modelInfo, fromFormat, providerFormat, suffixResult.HasSuffix) if err != nil { log.WithFields(log.Fields{ "provider": providerFormat, diff --git a/internal/thinking/validate.go b/internal/thinking/validate.go index 5ce113f7..f082ad56 100644 --- a/internal/thinking/validate.go +++ b/internal/thinking/validate.go @@ -18,12 +18,14 @@ import ( // - Clamps budget values to model's allowed range // - When converting Budget -> Level for level-only models, clamps the derived standard level to the nearest supported level // (special values none/auto are preserved) +// - When config comes from a model suffix, strict budget validation is disabled (we clamp instead of error) // // Parameters: // - config: The thinking configuration to validate // - support: Model's ThinkingSupport properties (nil means no thinking support) // - fromFormat: Source provider format (used to determine strict validation rules) // - toFormat: Target provider format +// - fromSuffix: Whether config was sourced from model suffix // // Returns: // - Normalized ThinkingConfig with clamped values @@ -33,7 +35,7 @@ import ( // - Budget-only model + Level config → Level converted to Budget // - Level-only model + Budget config → Budget converted to Level // - Hybrid model → preserve original format -func ValidateConfig(config ThinkingConfig, modelInfo *registry.ModelInfo, fromFormat, toFormat string) (*ThinkingConfig, error) { +func ValidateConfig(config ThinkingConfig, modelInfo *registry.ModelInfo, fromFormat, toFormat string, fromSuffix bool) (*ThinkingConfig, error) { fromFormat, toFormat = strings.ToLower(strings.TrimSpace(fromFormat)), strings.ToLower(strings.TrimSpace(toFormat)) model := "unknown" support := (*registry.ThinkingSupport)(nil) @@ -52,7 +54,7 @@ func ValidateConfig(config ThinkingConfig, modelInfo *registry.ModelInfo, fromFo } allowClampUnsupported := isBudgetBasedProvider(fromFormat) && isLevelBasedProvider(toFormat) - strictBudget := fromFormat != "" && isSameProviderFamily(fromFormat, toFormat) + strictBudget := !fromSuffix && fromFormat != "" && isSameProviderFamily(fromFormat, toFormat) budgetDerivedFromLevel := false capability := detectModelCapability(modelInfo) @@ -238,7 +240,7 @@ func clampLevel(level ThinkingLevel, modelInfo *registry.ModelInfo, provider str log.WithFields(log.Fields{ "provider": provider, "model": model, - "original_level": string(level), + "original_value": string(level), "clamped_to": string(clamped), }).Debug("thinking: level clamped |") return clamped diff --git a/test/thinking_conversion_test.go b/test/thinking_conversion_test.go index 397bbbff..8f527193 100644 --- a/test/thinking_conversion_test.go +++ b/test/thinking_conversion_test.go @@ -1001,15 +1001,17 @@ func TestThinkingE2EMatrix_Suffix(t *testing.T) { includeThoughts: "true", expectErr: false, }, - // Case 85: Gemini to Gemini, budget 64000 → exceeds Max error + // Case 85: Gemini to Gemini, budget 64000 → clamped to Max { - name: "85", - from: "gemini", - to: "gemini", - model: "gemini-budget-model(64000)", - inputJSON: `{"model":"gemini-budget-model(64000)","contents":[{"role":"user","parts":[{"text":"hi"}]}]}`, - expectField: "", - expectErr: true, + name: "85", + from: "gemini", + to: "gemini", + model: "gemini-budget-model(64000)", + inputJSON: `{"model":"gemini-budget-model(64000)","contents":[{"role":"user","parts":[{"text":"hi"}]}]}`, + expectField: "generationConfig.thinkingConfig.thinkingBudget", + expectValue: "20000", + includeThoughts: "true", + expectErr: false, }, // Case 86: Claude to Claude, budget 8192 → passthrough thinking.budget_tokens { @@ -1022,20 +1024,21 @@ func TestThinkingE2EMatrix_Suffix(t *testing.T) { expectValue: "8192", expectErr: false, }, - // Case 87: Claude to Claude, budget 200000 → exceeds Max error + // Case 87: Claude to Claude, budget 200000 → clamped to Max { name: "87", from: "claude", to: "claude", model: "claude-budget-model(200000)", inputJSON: `{"model":"claude-budget-model(200000)","messages":[{"role":"user","content":"hi"}]}`, - expectField: "", - expectErr: true, + expectField: "thinking.budget_tokens", + expectValue: "128000", + expectErr: false, }, - // Case 88: Antigravity to Antigravity, budget 8192 → passthrough thinkingBudget + // Case 88: Gemini-CLI to Antigravity, budget 8192 → passthrough thinkingBudget { name: "88", - from: "antigravity", + from: "gemini-cli", to: "antigravity", model: "antigravity-budget-model(8192)", inputJSON: `{"model":"antigravity-budget-model(8192)","request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}]}}`, @@ -1044,15 +1047,17 @@ func TestThinkingE2EMatrix_Suffix(t *testing.T) { includeThoughts: "true", expectErr: false, }, - // Case 89: Antigravity to Antigravity, budget 64000 → exceeds Max error + // Case 89: Gemini-CLI to Antigravity, budget 64000 → clamped to Max { - name: "89", - from: "antigravity", - to: "antigravity", - model: "antigravity-budget-model(64000)", - inputJSON: `{"model":"antigravity-budget-model(64000)","request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}]}}`, - expectField: "", - expectErr: true, + name: "89", + from: "gemini-cli", + to: "antigravity", + model: "antigravity-budget-model(64000)", + inputJSON: `{"model":"antigravity-budget-model(64000)","request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}]}}`, + expectField: "request.generationConfig.thinkingConfig.thinkingBudget", + expectValue: "20000", + includeThoughts: "true", + expectErr: false, }, // iflow tests: glm-test and minimax-test (Cases 90-105) @@ -1236,45 +1241,53 @@ func TestThinkingE2EMatrix_Suffix(t *testing.T) { // Gemini Family Cross-Channel Consistency (Cases 106-114) // Tests that gemini/gemini-cli/antigravity as same API family should have consistent validation behavior - // Case 106: Gemini to Antigravity, budget 64000 → exceeds Max error (same family strict validation) + // Case 106: Gemini to Antigravity, budget 64000 (suffix) → clamped to Max { - name: "106", - from: "gemini", - to: "antigravity", - model: "gemini-budget-model(64000)", - inputJSON: `{"model":"gemini-budget-model(64000)","contents":[{"role":"user","parts":[{"text":"hi"}]}]}`, - expectField: "", - expectErr: true, + name: "106", + from: "gemini", + to: "antigravity", + model: "gemini-budget-model(64000)", + inputJSON: `{"model":"gemini-budget-model(64000)","contents":[{"role":"user","parts":[{"text":"hi"}]}]}`, + expectField: "request.generationConfig.thinkingConfig.thinkingBudget", + expectValue: "20000", + includeThoughts: "true", + expectErr: false, }, - // Case 107: Gemini to Gemini-CLI, budget 64000 → exceeds Max error (same family strict validation) + // Case 107: Gemini to Gemini-CLI, budget 64000 (suffix) → clamped to Max { - name: "107", - from: "gemini", - to: "gemini-cli", - model: "gemini-budget-model(64000)", - inputJSON: `{"model":"gemini-budget-model(64000)","contents":[{"role":"user","parts":[{"text":"hi"}]}]}`, - expectField: "", - expectErr: true, + name: "107", + from: "gemini", + to: "gemini-cli", + model: "gemini-budget-model(64000)", + inputJSON: `{"model":"gemini-budget-model(64000)","contents":[{"role":"user","parts":[{"text":"hi"}]}]}`, + expectField: "request.generationConfig.thinkingConfig.thinkingBudget", + expectValue: "20000", + includeThoughts: "true", + expectErr: false, }, - // Case 108: Gemini-CLI to Antigravity, budget 64000 → exceeds Max error (same family strict validation) + // Case 108: Gemini-CLI to Antigravity, budget 64000 (suffix) → clamped to Max { - name: "108", - from: "gemini-cli", - to: "antigravity", - model: "gemini-budget-model(64000)", - inputJSON: `{"model":"gemini-budget-model(64000)","request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}]}}`, - expectField: "", - expectErr: true, + name: "108", + from: "gemini-cli", + to: "antigravity", + model: "gemini-budget-model(64000)", + inputJSON: `{"model":"gemini-budget-model(64000)","request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}]}}`, + expectField: "request.generationConfig.thinkingConfig.thinkingBudget", + expectValue: "20000", + includeThoughts: "true", + expectErr: false, }, - // Case 109: Gemini-CLI to Gemini, budget 64000 → exceeds Max error (same family strict validation) + // Case 109: Gemini-CLI to Gemini, budget 64000 (suffix) → clamped to Max { - name: "109", - from: "gemini-cli", - to: "gemini", - model: "gemini-budget-model(64000)", - inputJSON: `{"model":"gemini-budget-model(64000)","request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}]}}`, - expectField: "", - expectErr: true, + name: "109", + from: "gemini-cli", + to: "gemini", + model: "gemini-budget-model(64000)", + inputJSON: `{"model":"gemini-budget-model(64000)","request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}]}}`, + expectField: "generationConfig.thinkingConfig.thinkingBudget", + expectValue: "20000", + includeThoughts: "true", + expectErr: false, }, // Case 110: Gemini to Antigravity, budget 8192 → passthrough (normal value) { @@ -2301,10 +2314,10 @@ func TestThinkingE2EMatrix_Body(t *testing.T) { expectField: "", expectErr: true, }, - // Case 88: Antigravity to Antigravity, thinkingBudget=8192 → passthrough + // Case 88: Gemini-CLI to Antigravity, thinkingBudget=8192 → passthrough { name: "88", - from: "antigravity", + from: "gemini-cli", to: "antigravity", model: "antigravity-budget-model", inputJSON: `{"model":"antigravity-budget-model","request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}],"generationConfig":{"thinkingConfig":{"thinkingBudget":8192}}}}`, @@ -2313,10 +2326,10 @@ func TestThinkingE2EMatrix_Body(t *testing.T) { includeThoughts: "true", expectErr: false, }, - // Case 89: Antigravity to Antigravity, thinkingBudget=64000 → exceeds Max error + // Case 89: Gemini-CLI to Antigravity, thinkingBudget=64000 → exceeds Max error { name: "89", - from: "antigravity", + from: "gemini-cli", to: "antigravity", model: "antigravity-budget-model", inputJSON: `{"model":"antigravity-budget-model","request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}],"generationConfig":{"thinkingConfig":{"thinkingBudget":64000}}}}`, @@ -2744,9 +2757,9 @@ func runThinkingTests(t *testing.T, cases []thinkingTestCase) { t.Fatalf("field %s: expected %q, got %q, body=%s", tc.expectField, tc.expectValue, actualValue, string(body)) } - if tc.includeThoughts != "" && (tc.to == "gemini" || tc.to == "antigravity") { + if tc.includeThoughts != "" && (tc.to == "gemini" || tc.to == "gemini-cli" || tc.to == "antigravity") { path := "generationConfig.thinkingConfig.includeThoughts" - if tc.to == "antigravity" { + if tc.to == "gemini-cli" || tc.to == "antigravity" { path = "request.generationConfig.thinkingConfig.includeThoughts" } itVal := gjson.GetBytes(body, path)