From e4f0c9fccacc7a00bf770f5a97e1d3ef6a42d424 Mon Sep 17 00:00:00 2001 From: SaneSanders <129727118+SaneSanders@users.noreply.github.com> Date: Thu, 24 Sep 2026 04:47:59 +0300 Subject: [PATCH] fix(limits): read Kimi Code 2 usage quotas --- src/main/services/LimitsService.ts | 42 +++++++++++++++++++++++++++- tests/limits-normalizer.test.mjs | 45 ++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/src/main/services/LimitsService.ts b/src/main/services/LimitsService.ts index aa929f43..875330cd 100644 --- a/src/main/services/LimitsService.ts +++ b/src/main/services/LimitsService.ts @@ -1009,7 +1009,13 @@ export function normalizeKimiLimits(raw: unknown): LimitWindow[] { const payload = isRecord(raw.data) ? raw.data : raw; if (payload.kind === "error") { const message = typeof payload.message === "string" ? payload.message.toLowerCase() : ""; - throw new LimitsAdapterError(message.includes("auth") || message.includes("login") ? "not-authenticated" : "protocol-error"); + throw new LimitsAdapterError( + message.includes("auth") || message.includes("login") + ? "not-authenticated" + : message.includes("timed out") + ? "timeout" + : "protocol-error" + ); } const windows: LimitWindow[] = []; const managedSummary = payload.summary; @@ -1086,6 +1092,40 @@ export function normalizeKimiLimits(raw: unknown): LimitWindow[] { } } + const managedUsages = isRecord(payload.quota) && isRecord(payload.quota.usages) ? payload.quota.usages : null; + if (managedUsages) { + const definitions: Array<{ + key: "limit5h" | "limit7d" | "monthTotal"; + id: string; + slot: "primary" | "secondary"; + label: string; + windowMinutes: number | null; + }> = [ + { key: "limit5h", id: "kimi:managed:300", slot: "primary", label: "5h", windowMinutes: 300 }, + { key: "limit7d", id: "kimi:weekly", slot: "secondary", label: "7d", windowMinutes: 10_080 }, + { key: "monthTotal", id: "kimi:monthly", slot: "secondary", label: "monthly", windowMinutes: null } + ]; + for (const { key, id, slot, label, windowMinutes } of definitions) { + const candidate = managedUsages[key]; + if (!isRecord(candidate)) continue; + const usedRatio = numericValue(candidate.usedRatio); + const resetsAt = epochMilliseconds(candidate.resetAt); + if (usedRatio === null && resetsAt === null) continue; + windows.push({ + id, + bucketId: "kimi", + slot, + isDefaultBucket: true, + label, + usedPercent: usedRatio === null ? null : clampPercent(usedRatio * 100), + used: null, + limit: null, + windowMinutes, + resetsAt + }); + } + } + return windows.slice(0, MAX_WINDOWS); } diff --git a/tests/limits-normalizer.test.mjs b/tests/limits-normalizer.test.mjs index af5963f5..266b4773 100644 --- a/tests/limits-normalizer.test.mjs +++ b/tests/limits-normalizer.test.mjs @@ -97,6 +97,51 @@ test("normalizes Kimi weekly and rolling usage without exposing account metadata ]); }); +test("normalizes Kimi Code 2 quota windows", () => { + const windows = normalizeKimiLimits({ + code: 0, + msg: "success", + data: { + kind: "ok", + quota: { + usages: { + limit5h: { usedRatio: 0.25, resetAt: "2026-09-24T05:45:52Z" }, + limit7d: { usedRatio: 0.5, resetAt: "2026-09-29T00:00:00Z" }, + monthTotal: { usedRatio: 0.1246, resetAt: "2026-10-18T00:00:00Z" }, + monthCode: { usedRatio: 0.1, resetAt: "2026-10-18T00:00:00Z" } + }, + extraUsage: null + } + } + }); + + assert.deepEqual(windows.map((window) => ({ + id: window.id, + usedPercent: window.usedPercent, + windowMinutes: window.windowMinutes, + resetsAt: window.resetsAt + })), [ + { id: "kimi:managed:300", usedPercent: 25, windowMinutes: 300, resetsAt: Date.parse("2026-09-24T05:45:52Z") }, + { id: "kimi:weekly", usedPercent: 50, windowMinutes: 10_080, resetsAt: Date.parse("2026-09-29T00:00:00Z") }, + { id: "kimi:monthly", usedPercent: 12.46, windowMinutes: null, resetsAt: Date.parse("2026-10-18T00:00:00Z") } + ]); +}); + +test("maps Kimi usage errors to reasons", () => { + const cases = [ + { message: "Failed to fetch usage: request timed out.", reason: "timeout" }, + { message: "Authorization failed. Please check your API key (try /login).", reason: "not-authenticated" }, + { message: "Failed to fetch usage: HTTP 500", reason: "protocol-error" } + ]; + + for (const { message, reason } of cases) { + assert.throws( + () => normalizeKimiLimits({ code: 0, data: { kind: "error", message } }), + (error) => error instanceof Error && error.message === reason + ); + } +}); + test("normalizes every real OpenCode Go usage window", () => { const windows = normalizeOpenCodeGoLimits({ usage: {