From db4f1ceff43af495db6321a2cb912557070d3e9f Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Fri, 10 Jul 2026 00:12:18 +0800 Subject: [PATCH] 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. --- .../thinking/kimi_max_clamp_repro_test.go | 33 +++++++++++++++++++ internal/thinking/validate.go | 20 +++++++++-- test/thinking_conversion_test.go | 27 +++++++++++++++ 3 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 internal/thinking/kimi_max_clamp_repro_test.go diff --git a/internal/thinking/kimi_max_clamp_repro_test.go b/internal/thinking/kimi_max_clamp_repro_test.go new file mode 100644 index 000000000..d5d3ff6ed --- /dev/null +++ b/internal/thinking/kimi_max_clamp_repro_test.go @@ -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) + } +} diff --git a/internal/thinking/validate.go b/internal/thinking/validate.go index 7a7a8fa66..2352862f6 100644 --- a/internal/thinking/validate.go +++ b/internal/thinking/validate.go @@ -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) diff --git a/test/thinking_conversion_test.go b/test/thinking_conversion_test.go index b959b385c..1520dc249 100644 --- a/test/thinking_conversion_test.go +++ b/test/thinking_conversion_test.go @@ -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)