From 6a3856b6395a1e1c6c7bb878f9ae2e625c6dafba Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 20 Jun 2026 19:10:40 +0700 Subject: [PATCH] Fix Kimi usage credential lookup --- docs/providers.md | 2 + .../services/quota-fetcher/providers/kimi.ts | 25 +++++++--- .../services/quota-fetcher/service.test.ts | 48 ++++++++++++++++++- 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/docs/providers.md b/docs/providers.md index 0054d1218..3f41aba6e 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -67,6 +67,8 @@ To add plan usage for a provider, add `packages/server/src/services/quota-fetche Keep the protocol shape provider-agnostic. Do not add provider-specific renderers for new limit windows; labels and generic bars should carry the UI. API responses should be parsed and normalized with Zod inside the fetcher, while the protocol boundary stays strict so old/new client compatibility is explicit. +Kimi Code usage follows the CLI-managed credential file at `KIMI_CODE_HOME` or `~/.kimi-code/credentials/kimi-code.json`; do not probe the legacy `~/.kimi` path as the primary source for current Kimi Code installs. + --- ## ACP Provider Checklist diff --git a/packages/server/src/services/quota-fetcher/providers/kimi.ts b/packages/server/src/services/quota-fetcher/providers/kimi.ts index 5c46f8ff3..7fb8e6db5 100644 --- a/packages/server/src/services/quota-fetcher/providers/kimi.ts +++ b/packages/server/src/services/quota-fetcher/providers/kimi.ts @@ -88,13 +88,24 @@ export class KimiQuotaProvider implements ProviderUsageFetcher { } private async readKimiToken(): Promise { - const path = join(homedir(), ".kimi", "credentials", "kimi-code.json"); - if (!existsSync(path)) return null; - try { - const credentials = KimiAuthSchema.parse(JSON.parse(await fs.readFile(path, "utf8"))); - return credentials.access_token ?? null; - } catch { - return null; + const paths = [ + join( + process.env["KIMI_CODE_HOME"] || join(homedir(), ".kimi-code"), + "credentials", + "kimi-code.json", + ), + join(homedir(), ".kimi", "credentials", "kimi-code.json"), + ]; + + for (const path of paths) { + if (!existsSync(path)) continue; + try { + const credentials = KimiAuthSchema.parse(JSON.parse(await fs.readFile(path, "utf8"))); + if (credentials.access_token) return credentials.access_token; + } catch { + continue; + } } + return null; } } diff --git a/packages/server/src/services/quota-fetcher/service.test.ts b/packages/server/src/services/quota-fetcher/service.test.ts index 2c3f41686..cd0cdf077 100644 --- a/packages/server/src/services/quota-fetcher/service.test.ts +++ b/packages/server/src/services/quota-fetcher/service.test.ts @@ -1,4 +1,4 @@ -import { readFileSync, writeFileSync } from "node:fs"; +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -36,6 +36,20 @@ function writeCodexAuth(dir: string, accessToken: string, refreshToken = "rt_cod ); } +function writeKimiCredentials(dir: string, accessToken: string): void { + mkdirSync(join(dir, "credentials"), { recursive: true }); + writeFileSync( + join(dir, "credentials", "kimi-code.json"), + JSON.stringify({ + access_token: accessToken, + refresh_token: "rt_kimi", + expires_at: 1_798_812_800, + scope: "kimi-code", + token_type: "Bearer", + }), + ); +} + function makeClaudeResponse( overrides: Partial<{ five_hour: { utilization: number | string; resets_at: string }; @@ -299,6 +313,7 @@ describe("real provider usage fetchers", () => { "GROK_TOKEN", "KIMI_TOKEN", "KIMI_API_KEY", + "KIMI_CODE_HOME", "CODEX_HOME", ]) { delete process.env[key]; @@ -694,4 +709,35 @@ describe("real provider usage fetchers", () => { ], }); }); + + it("fetches Kimi usage from the CLI credential home", async () => { + writeKimiCredentials(join(homeDir, ".kimi-code"), "kimi_cli_token"); + fetchApi = vi.fn(async (url: RequestInfo | URL, init?: RequestInit) => { + expect(url.toString()).toBe("https://api.kimi.com/coding/v1/usages"); + expect((init?.headers as Record | undefined)?.Authorization).toBe( + "Bearer kimi_cli_token", + ); + return jsonResponse({ + usage: { + limit: "200", + remaining: "150", + resetTime: "2026-06-23T05:12:17Z", + }, + }); + }) as unknown as typeof fetch; + + const kimi = findProvider(await service().listUsage(), "kimi"); + + expect(kimi).toMatchObject({ + status: "available", + windows: [ + expect.objectContaining({ + id: "coding_usage", + usedPct: 25, + remainingPct: 75, + resetsAt: "2026-06-23T05:12:17Z", + }), + ], + }); + }); });