From 8bafd854336df25e3f0bad2b0849641280091273 Mon Sep 17 00:00:00 2001 From: mikewong23571 Date: Sat, 18 Jul 2026 05:33:12 -0700 Subject: [PATCH] fix(executor): normalize Kimi upstream model ID to canonical k3 Claude Code sends the configured model name (e.g. kimi-k3[1m]) to CLIProxyAPI. The Kimi executor forwarded this internal ID to Kimi's https://api.kimi.com/coding endpoint. Kimi treats kimi-k3 and kimi-k3[1m] as 256K-context models, while only bare k3 supports the full 1M context window. Normalize the upstream model by stripping the CLIProxyAPI kimi- prefix and any Claude Code [1m] suffix while preserving a trailing thinking suffix (e.g. (1024)), so that requests for kimi-k3, kimi-k3[1m], kimi-k3(1024), and kimi-k3[1m](1024) are all sent to Kimi as k3 or k3(1024). This applies to: - Execute (Claude and OpenAI branches) - ExecuteStream (Claude and OpenAI branches) - CountTokens Validation (using the stored Kimi token): - GET /v1/models shows k3 with context_length 1048576. - Direct /v1/messages calls with ~270k input tokens: - model=k3 succeeds. - model=k3[1m], kimi-k3, kimi-k3[1m] fail with: 'Your request exceeded model token limit: 262144 (requested: 270014)'. Add unit tests for normalizeKimiUpstreamModel. Closes #4418 Co-Authored-By: Claude Fable 5 --- internal/runtime/executor/kimi_executor.go | 29 ++++++++++++++++--- .../runtime/executor/kimi_executor_test.go | 26 +++++++++++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/internal/runtime/executor/kimi_executor.go b/internal/runtime/executor/kimi_executor.go index 3d7b13e2..d48781c0 100644 --- a/internal/runtime/executor/kimi_executor.go +++ b/internal/runtime/executor/kimi_executor.go @@ -77,6 +77,7 @@ func (e *KimiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req from := opts.SourceFormat if from.String() == "claude" { auth.Attributes["base_url"] = kimiauth.KimiAPIBaseURL + req.Model = normalizeKimiUpstreamModel(req.Model) return e.ClaudeExecutor.Execute(ctx, auth, req, opts) } responseFormat := cliproxyexecutor.ResponseFormatOrSource(opts) @@ -97,8 +98,8 @@ func (e *KimiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false) body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false) - // Strip kimi- prefix for upstream API - upstreamModel := stripKimiPrefix(baseModel) + // Strip kimi- prefix and any [1m] suffix for upstream API + upstreamModel := normalizeKimiUpstreamModel(baseModel) body, err = sjson.SetBytes(body, "model", upstreamModel) if err != nil { return resp, fmt.Errorf("kimi executor: failed to set model in payload: %w", err) @@ -187,6 +188,7 @@ func (e *KimiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut from := opts.SourceFormat if from.String() == "claude" { auth.Attributes["base_url"] = kimiauth.KimiAPIBaseURL + req.Model = normalizeKimiUpstreamModel(req.Model) return e.ClaudeExecutor.ExecuteStream(ctx, auth, req, opts) } responseFormat := cliproxyexecutor.ResponseFormatOrSource(opts) @@ -206,8 +208,8 @@ func (e *KimiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true) body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true) - // Strip kimi- prefix for upstream API - upstreamModel := stripKimiPrefix(baseModel) + // Strip kimi- prefix and any [1m] suffix for upstream API + upstreamModel := normalizeKimiUpstreamModel(baseModel) body, err = sjson.SetBytes(body, "model", upstreamModel) if err != nil { return nil, fmt.Errorf("kimi executor: failed to set model in payload: %w", err) @@ -327,6 +329,7 @@ func (e *KimiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut // CountTokens estimates token count for Kimi requests. func (e *KimiExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (cliproxyexecutor.Response, error) { auth.Attributes["base_url"] = kimiauth.KimiAPIBaseURL + req.Model = normalizeKimiUpstreamModel(req.Model) return e.ClaudeExecutor.CountTokens(ctx, auth, req, opts) } @@ -754,3 +757,21 @@ func stripKimiPrefix(model string) string { } return model } + +// normalizeKimiUpstreamModel returns the canonical upstream model ID for Kimi. +// It strips the CLIProxyAPI "kimi-" prefix and any Claude Code "[1m]" context +// suffix while preserving a trailing thinking suffix (e.g. "(1024)"), so that +// the upstream API receives IDs such as "k3(1024)" instead of "kimi-k3[1m](1024)". +func normalizeKimiUpstreamModel(model string) string { + model = strings.TrimSpace(model) + parsed := thinking.ParseSuffix(model) + base := parsed.ModelName + if strings.HasSuffix(strings.ToLower(base), "[1m]") { + base = base[:len(base)-len("[1m]")] + } + normalized := strings.ToLower(stripKimiPrefix(strings.TrimSpace(base))) + if parsed.HasSuffix { + return normalized + "(" + parsed.RawSuffix + ")" + } + return normalized +} diff --git a/internal/runtime/executor/kimi_executor_test.go b/internal/runtime/executor/kimi_executor_test.go index f3de70f1..3a8d0e11 100644 --- a/internal/runtime/executor/kimi_executor_test.go +++ b/internal/runtime/executor/kimi_executor_test.go @@ -270,3 +270,29 @@ func TestNormalizeKimiToolMessageLinks_PreservesAssistantWithToolLinkOrReasoning t.Fatalf("messages.3.content.0.text = %q, want %q", got, " visible ") } } + +func TestNormalizeKimiUpstreamModel(t *testing.T) { + cases := []struct { + in string + want string + }{ + {"kimi-k3[1m]", "k3"}, + {"kimi-k3", "k3"}, + {"Kimi-K3[1M]", "k3"}, + {"k3[1m]", "k3"}, + {"k3", "k3"}, + {"kimi-k2.6", "k2.6"}, + {"kimi-k2.6[1m]", "k2.6"}, + {"kimi-k3(1024)", "k3(1024)"}, + {"kimi-k3[1m](1024)", "k3(1024)"}, + {"kimi-k2.6(high)", "k2.6(high)"}, + {"kimi-k2.6[1m](high)", "k2.6(high)"}, + } + + for _, c := range cases { + got := normalizeKimiUpstreamModel(c.in) + if got != c.want { + t.Errorf("normalizeKimiUpstreamModel(%q) = %q, want %q", c.in, got, c.want) + } + } +}