feat(validation): enhance cross-family level clamping with model type mismatch handling

- Added `modelFamilyMismatch` check to improve validation for provider families reusing Claude-compatible formats (e.g., Kimi models).
- Adjusted `allowClampUnsupported` logic to account for mismatched model families.
- Updated `strictBudget` validation to exclude mismatched model types.
- Added test cases to verify clamping behavior for Kimi models serving Claude-compatible requests.
This commit is contained in:
Luis Pater
2026-07-10 00:12:18 +08:00
parent ee71dc52b7
commit db4f1ceff4
3 changed files with 78 additions and 2 deletions

View File

@@ -0,0 +1,33 @@
package thinking_test
import (
"testing"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
_ "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking/provider/claude"
_ "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking/provider/kimi"
"github.com/tidwall/gjson"
)
// Reproduces Claude Code -> Kimi /v1/messages with effort=max.
// KimiExecutor delegates to ClaudeExecutor, so ApplyThinking sees claude/claude.
func TestKimiClaudeMessagesMaxClampsToHigh(t *testing.T) {
models := registry.GetKimiModels()
reg := registry.GetGlobalRegistry()
clientID := "test-kimi-max-clamp"
reg.RegisterClient(clientID, "kimi", models)
t.Cleanup(func() { reg.UnregisterClient(clientID) })
body := []byte(`{"model":"kimi-k2.5","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"},"output_config":{"effort":"max"}}`)
out, err := thinking.ApplyThinking(body, "kimi-k2.5", "claude", "claude", "claude")
if err != nil {
t.Fatalf("ApplyThinking returned error: %v", err)
}
if got := gjson.GetBytes(out, "thinking.type").String(); got != "adaptive" {
t.Fatalf("thinking.type = %q, want adaptive", got)
}
if got := gjson.GetBytes(out, "output_config.effort").String(); got != "high" {
t.Fatalf("output_config.effort = %q, want high", got)
}
}

View File

@@ -56,15 +56,31 @@ func ValidateConfig(config ThinkingConfig, modelInfo *registry.ModelInfo, fromFo
// 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.
//
// modelFamilyMismatch covers providers that reuse another protocol on the wire
// (e.g. Kimi serving Claude-compatible /v1/messages). In that path fromFormat and
// toFormat both look like "claude", but the model itself is not Claude-family, so
// unsupported levels such as "max" should clamp to the nearest supported level
// (typically "high") instead of failing validation.
toCapability := detectModelCapability(modelInfo)
toHasLevelSupport := toCapability == CapabilityLevelOnly || toCapability == CapabilityHybrid
allowClampUnsupported := toHasLevelSupport && !isSameProviderFamily(fromFormat, toFormat)
modelFamilyMismatch := false
if modelInfo != nil {
modelType := strings.ToLower(strings.TrimSpace(modelInfo.Type))
if modelType != "" {
if (fromFormat != "" && !isSameProviderFamily(fromFormat, modelType)) ||
(toFormat != "" && !isSameProviderFamily(toFormat, modelType)) {
modelFamilyMismatch = true
}
}
}
allowClampUnsupported := toHasLevelSupport && (!isSameProviderFamily(fromFormat, toFormat) || modelFamilyMismatch)
// 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) && !modelFamilyMismatch
budgetDerivedFromLevel := false
capability := detectModelCapability(modelInfo)

View File

@@ -2887,6 +2887,33 @@ func TestThinkingE2EClaudeAdaptive_Body(t *testing.T) {
inputJSON: `{"model":"claude-sonnet-4-6-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"},"output_config":{"effort":"xhigh"}}`,
expectErr: true,
},
// Kimi models exposed via Claude-compatible /v1/messages keep wire format
// claude→claude, but the model type is kimi. Claude Code often sends
// effort=max; clamp to the highest Kimi-supported level (high).
{
name: "C28",
from: "claude",
to: "claude",
model: "kimi-level-model",
inputJSON: `{"model":"kimi-level-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"},"output_config":{"effort":"max"}}`,
expectField: "thinking.type",
expectValue: "adaptive",
expectField2: "output_config.effort",
expectValue2: "high",
expectErr: false,
},
{
name: "C29",
from: "claude",
to: "claude",
model: "kimi-level-model",
inputJSON: `{"model":"kimi-level-model","messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"},"output_config":{"effort":"xhigh"}}`,
expectField: "thinking.type",
expectValue: "adaptive",
expectField2: "output_config.effort",
expectValue2: "high",
expectErr: false,
},
}
runThinkingTests(t, cases)