From 1187aa822259ba5ffd5bc1e1523e26d12be9ca16 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Fri, 6 Feb 2026 21:28:40 +0800 Subject: [PATCH 1/2] feat(translator): capture cached token count in usage metadata and handle prompt caching - Added support to extract and include `cachedContentTokenCount` in `usage.prompt_tokens_details`. - Logged warnings for failures to set cached token count for better debugging. --- .../chat-completions/gemini-cli_openai_response.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go index 5a1faf51..97c18c1e 100644 --- a/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go +++ b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go @@ -14,6 +14,7 @@ import ( "time" . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/openai/chat-completions" + log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) @@ -85,6 +86,7 @@ func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJ // Extract and set usage metadata (token counts). if usageResult := gjson.GetBytes(rawJSON, "response.usageMetadata"); usageResult.Exists() { + cachedTokenCount := usageResult.Get("cachedContentTokenCount").Int() if candidatesTokenCountResult := usageResult.Get("candidatesTokenCount"); candidatesTokenCountResult.Exists() { template, _ = sjson.Set(template, "usage.completion_tokens", candidatesTokenCountResult.Int()) } @@ -97,6 +99,14 @@ func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJ if thoughtsTokenCount > 0 { template, _ = sjson.Set(template, "usage.completion_tokens_details.reasoning_tokens", thoughtsTokenCount) } + // Include cached token count if present (indicates prompt caching is working) + if cachedTokenCount > 0 { + var err error + template, err = sjson.Set(template, "usage.prompt_tokens_details.cached_tokens", cachedTokenCount) + if err != nil { + log.Warnf("antigravity openai response: failed to set cached_tokens: %v", err) + } + } } // Process the main content part of the response. From fc7b6ef086e3a91773f115c9a284d04e2fc0f78b Mon Sep 17 00:00:00 2001 From: LTbinglingfeng Date: Sat, 7 Feb 2026 01:16:39 +0800 Subject: [PATCH 2/2] fix(kimi): add OAuth model-alias channel support and cover OAuth excluded-models with tests --- config.example.yaml | 7 ++- sdk/cliproxy/auth/oauth_model_alias.go | 4 +- sdk/cliproxy/auth/oauth_model_alias_test.go | 19 ++++++++ .../service_oauth_excluded_models_test.go | 45 +++++++++++++++++++ .../service_oauth_model_alias_test.go | 24 ++++++++++ 5 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 sdk/cliproxy/service_oauth_excluded_models_test.go diff --git a/config.example.yaml b/config.example.yaml index 75e0030c..1c48e02d 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -221,7 +221,7 @@ nonstream-keepalive-interval: 0 # Global OAuth model name aliases (per channel) # These aliases rename model IDs for both model listing and request routing. -# Supported channels: gemini-cli, vertex, aistudio, antigravity, claude, codex, qwen, iflow. +# Supported channels: gemini-cli, vertex, aistudio, antigravity, claude, codex, qwen, iflow, kimi. # NOTE: Aliases do not apply to gemini-api-key, codex-api-key, claude-api-key, openai-compatibility, vertex-api-key, or ampcode. # You can repeat the same name with different aliases to expose multiple client model names. oauth-model-alias: @@ -262,6 +262,9 @@ oauth-model-alias: # iflow: # - name: "glm-4.7" # alias: "glm-god" +# kimi: +# - name: "kimi-k2.5" +# alias: "k2.5" # OAuth provider excluded models # oauth-excluded-models: @@ -284,6 +287,8 @@ oauth-model-alias: # - "vision-model" # iflow: # - "tstars2.0" +# kimi: +# - "kimi-k2-thinking" # Optional payload configuration # payload: diff --git a/sdk/cliproxy/auth/oauth_model_alias.go b/sdk/cliproxy/auth/oauth_model_alias.go index 4111663e..d5d2ff8a 100644 --- a/sdk/cliproxy/auth/oauth_model_alias.go +++ b/sdk/cliproxy/auth/oauth_model_alias.go @@ -221,7 +221,7 @@ func modelAliasChannel(auth *Auth) string { // and auth kind. Returns empty string if the provider/authKind combination doesn't support // OAuth model alias (e.g., API key authentication). // -// Supported channels: gemini-cli, vertex, aistudio, antigravity, claude, codex, qwen, iflow. +// Supported channels: gemini-cli, vertex, aistudio, antigravity, claude, codex, qwen, iflow, kimi. func OAuthModelAliasChannel(provider, authKind string) string { provider = strings.ToLower(strings.TrimSpace(provider)) authKind = strings.ToLower(strings.TrimSpace(authKind)) @@ -245,7 +245,7 @@ func OAuthModelAliasChannel(provider, authKind string) string { return "" } return "codex" - case "gemini-cli", "aistudio", "antigravity", "qwen", "iflow": + case "gemini-cli", "aistudio", "antigravity", "qwen", "iflow", "kimi": return provider default: return "" diff --git a/sdk/cliproxy/auth/oauth_model_alias_test.go b/sdk/cliproxy/auth/oauth_model_alias_test.go index 6956411c..32390959 100644 --- a/sdk/cliproxy/auth/oauth_model_alias_test.go +++ b/sdk/cliproxy/auth/oauth_model_alias_test.go @@ -70,6 +70,15 @@ func TestResolveOAuthUpstreamModel_SuffixPreservation(t *testing.T) { input: "gemini-2.5-pro(none)", want: "gemini-2.5-pro-exp-03-25(none)", }, + { + name: "kimi suffix preserved", + aliases: map[string][]internalconfig.OAuthModelAlias{ + "kimi": {{Name: "kimi-k2.5", Alias: "k2.5"}}, + }, + channel: "kimi", + input: "k2.5(high)", + want: "kimi-k2.5(high)", + }, { name: "case insensitive alias lookup with suffix", aliases: map[string][]internalconfig.OAuthModelAlias{ @@ -152,11 +161,21 @@ func createAuthForChannel(channel string) *Auth { return &Auth{Provider: "qwen"} case "iflow": return &Auth{Provider: "iflow"} + case "kimi": + return &Auth{Provider: "kimi"} default: return &Auth{Provider: channel} } } +func TestOAuthModelAliasChannel_Kimi(t *testing.T) { + t.Parallel() + + if got := OAuthModelAliasChannel("kimi", "oauth"); got != "kimi" { + t.Fatalf("OAuthModelAliasChannel() = %q, want %q", got, "kimi") + } +} + func TestApplyOAuthModelAlias_SuffixPreservation(t *testing.T) { t.Parallel() diff --git a/sdk/cliproxy/service_oauth_excluded_models_test.go b/sdk/cliproxy/service_oauth_excluded_models_test.go new file mode 100644 index 00000000..56315248 --- /dev/null +++ b/sdk/cliproxy/service_oauth_excluded_models_test.go @@ -0,0 +1,45 @@ +package cliproxy + +import ( + "testing" + + "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" +) + +func TestOAuthExcludedModels_KimiOAuth(t *testing.T) { + t.Parallel() + + svc := &Service{ + cfg: &config.Config{ + OAuthExcludedModels: map[string][]string{ + "kimi": {"kimi-k2-thinking", "kimi-k2.5"}, + }, + }, + } + + got := svc.oauthExcludedModels("kimi", "oauth") + if len(got) != 2 { + t.Fatalf("expected 2 excluded models, got %d", len(got)) + } + if got[0] != "kimi-k2-thinking" || got[1] != "kimi-k2.5" { + t.Fatalf("unexpected excluded models: %#v", got) + } +} + +func TestOAuthExcludedModels_KimiAPIKeyReturnsNil(t *testing.T) { + t.Parallel() + + svc := &Service{ + cfg: &config.Config{ + OAuthExcludedModels: map[string][]string{ + "kimi": {"kimi-k2-thinking"}, + }, + }, + } + + got := svc.oauthExcludedModels("kimi", "apikey") + if got != nil { + t.Fatalf("expected nil for apikey auth kind, got %#v", got) + } +} + diff --git a/sdk/cliproxy/service_oauth_model_alias_test.go b/sdk/cliproxy/service_oauth_model_alias_test.go index 2caf7a17..e7c58058 100644 --- a/sdk/cliproxy/service_oauth_model_alias_test.go +++ b/sdk/cliproxy/service_oauth_model_alias_test.go @@ -90,3 +90,27 @@ func TestApplyOAuthModelAlias_ForkAddsMultipleAliases(t *testing.T) { t.Fatalf("expected forked model name %q, got %q", "models/g5-2", out[2].Name) } } + +func TestApplyOAuthModelAlias_KimiRename(t *testing.T) { + cfg := &config.Config{ + OAuthModelAlias: map[string][]config.OAuthModelAlias{ + "kimi": { + {Name: "kimi-k2.5", Alias: "k2.5"}, + }, + }, + } + models := []*ModelInfo{ + {ID: "kimi-k2.5", Name: "models/kimi-k2.5"}, + } + + out := applyOAuthModelAlias(cfg, "kimi", "oauth", models) + if len(out) != 1 { + t.Fatalf("expected 1 model, got %d", len(out)) + } + if out[0].ID != "k2.5" { + t.Fatalf("expected model id %q, got %q", "k2.5", out[0].ID) + } + if out[0].Name != "models/k2.5" { + t.Fatalf("expected model name %q, got %q", "models/k2.5", out[0].Name) + } +}