feat(thinking): improve provider family checks and clamp unsupported levels

This commit is contained in:
hkfires
2026-03-03 19:05:15 +08:00
parent ce87714ef1
commit c80ab8bf0d
2 changed files with 42 additions and 18 deletions

View File

@@ -53,7 +53,17 @@ func ValidateConfig(config ThinkingConfig, modelInfo *registry.ModelInfo, fromFo
return &config, nil return &config, nil
} }
allowClampUnsupported := isBudgetBasedProvider(fromFormat) && isLevelBasedProvider(toFormat) // allowClampUnsupported determines whether to clamp unsupported levels instead of returning an error.
// This applies when crossing provider families (e.g., openai→gemini, claude→gemini) and the target
// model supports discrete levels. Same-family conversions require strict validation.
toCapability := detectModelCapability(modelInfo)
toHasLevelSupport := toCapability == CapabilityLevelOnly || toCapability == CapabilityHybrid
allowClampUnsupported := toHasLevelSupport && !isSameProviderFamily(fromFormat, toFormat)
// strictBudget determines whether to enforce strict budget range validation.
// This applies when: (1) config comes from request body (not suffix), (2) source format is known,
// and (3) source and target are in the same provider family. Cross-family or suffix-based configs
// are clamped instead of rejected to improve interoperability.
strictBudget := !fromSuffix && fromFormat != "" && isSameProviderFamily(fromFormat, toFormat) strictBudget := !fromSuffix && fromFormat != "" && isSameProviderFamily(fromFormat, toFormat)
budgetDerivedFromLevel := false budgetDerivedFromLevel := false
@@ -352,11 +362,21 @@ func isGeminiFamily(provider string) bool {
} }
} }
func isOpenAIFamily(provider string) bool {
switch provider {
case "openai", "openai-response", "codex":
return true
default:
return false
}
}
func isSameProviderFamily(from, to string) bool { func isSameProviderFamily(from, to string) bool {
if from == to { if from == to {
return true return true
} }
return isGeminiFamily(from) && isGeminiFamily(to) return (isGeminiFamily(from) && isGeminiFamily(to)) ||
(isOpenAIFamily(from) && isOpenAIFamily(to))
} }
func abs(x int) int { func abs(x int) int {

View File

@@ -386,15 +386,17 @@ func TestThinkingE2EMatrix_Suffix(t *testing.T) {
includeThoughts: "true", includeThoughts: "true",
expectErr: false, expectErr: false,
}, },
// Case 30: Effort xhigh → not in low/high → error // Case 30: Effort xhigh → clamped to high
{ {
name: "30", name: "30",
from: "openai", from: "openai",
to: "gemini", to: "gemini",
model: "gemini-mixed-model(xhigh)", model: "gemini-mixed-model(xhigh)",
inputJSON: `{"model":"gemini-mixed-model(xhigh)","messages":[{"role":"user","content":"hi"}]}`, inputJSON: `{"model":"gemini-mixed-model(xhigh)","messages":[{"role":"user","content":"hi"}]}`,
expectField: "", expectField: "generationConfig.thinkingConfig.thinkingLevel",
expectErr: true, expectValue: "high",
includeThoughts: "true",
expectErr: false,
}, },
// Case 31: Effort none → clamped to low (min supported) → includeThoughts=false // Case 31: Effort none → clamped to low (min supported) → includeThoughts=false
{ {
@@ -1668,15 +1670,17 @@ func TestThinkingE2EMatrix_Body(t *testing.T) {
includeThoughts: "true", includeThoughts: "true",
expectErr: false, expectErr: false,
}, },
// Case 30: reasoning_effort=xhigh → error (not in low/high) // Case 30: reasoning_effort=xhigh → clamped to high
{ {
name: "30", name: "30",
from: "openai", from: "openai",
to: "gemini", to: "gemini",
model: "gemini-mixed-model", model: "gemini-mixed-model",
inputJSON: `{"model":"gemini-mixed-model","messages":[{"role":"user","content":"hi"}],"reasoning_effort":"xhigh"}`, inputJSON: `{"model":"gemini-mixed-model","messages":[{"role":"user","content":"hi"}],"reasoning_effort":"xhigh"}`,
expectField: "", expectField: "generationConfig.thinkingConfig.thinkingLevel",
expectErr: true, expectValue: "high",
includeThoughts: "true",
expectErr: false,
}, },
// Case 31: reasoning_effort=none → clamped to low → includeThoughts=false // Case 31: reasoning_effort=none → clamped to low → includeThoughts=false
{ {