mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-09-03 06:35:00 +08:00
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:
33
internal/thinking/kimi_max_clamp_repro_test.go
Normal file
33
internal/thinking/kimi_max_clamp_repro_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user