Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion src/main/services/LimitsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

Expand Down
45 changes: 45 additions & 0 deletions tests/limits-normalizer.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down