fix(kimi): surface the 5h usage window reset time

The Kimi Code usages endpoint returns resetTime on each limit's detail
object, while window carries only duration/timeUnit. buildWindow() only
reads window fields, so the 5h row parsed to a window with durationMs but
no resetsAt — and omp usage renders "resets in …" only when
window.resetsAt is set. The Total quota row worked because toUsageLimit
falls back to row.resetsAt when no window exists, but the row.window ??
short-circuit bypassed that fallback for the 5h row.

Carry the row-level reset onto the window when the window itself has
none; an explicit window resetTime still wins.
This commit is contained in:
iacore
2026-07-18 11:48:20 +08:00
parent 1b4a4fd3fd
commit 85e122cece
3 changed files with 88 additions and 4 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed Kimi Code usage reports dropping the 5h window reset time (`omp usage` showed no "resets in …" for the 5h limit): the API returns `resetTime` on the limit `detail`, not on `window`, so the parsed row-level reset is now carried onto the window when the window itself has none.
## [17.0.3] - 2026-07-17
### Fixed
+10 -4
View File
@@ -144,15 +144,21 @@ function buildUsageStatus(amount: UsageAmount): UsageStatus {
}
function toUsageLimit(row: KimiUsageRow, provider: string, index: number, accountId?: string): UsageLimit {
const window: UsageWindow | undefined =
row.window ??
(row.resetsAt
// Kimi puts `resetTime` on the limit `detail`, not on `window`, so a
// window built from `duration`/`timeUnit` alone carries no resetsAt.
// Fall back to the row-level reset so `omp usage` can render
// "resets in …" for the 5h window too.
const window: UsageWindow | undefined = row.window
? row.window.resetsAt !== undefined || row.resetsAt === undefined
? row.window
: { ...row.window, resetsAt: row.resetsAt }
: row.resetsAt
? {
id: "default",
label: "Usage window",
resetsAt: row.resetsAt,
}
: undefined);
: undefined;
const amount = buildUsageAmount(row);
return {
+74
View File
@@ -0,0 +1,74 @@
import { describe, expect, it } from "bun:test";
import type { FetchImpl } from "@oh-my-pi/pi-ai/types";
import type { UsageFetchContext, UsageFetchParams } from "@oh-my-pi/pi-ai/usage";
import { kimiUsageProvider } from "@oh-my-pi/pi-ai/usage/kimi";
function makeCredential(): UsageFetchParams["credential"] {
return {
type: "oauth",
accessToken: "kimi-test-token",
};
}
function makeCtx(payload: unknown): UsageFetchContext {
const fetch: FetchImpl = async () =>
new Response(JSON.stringify(payload), {
status: 200,
headers: { "content-type": "application/json" },
});
return { fetch };
}
describe("kimi usage provider", () => {
it("surfaces the 5h limit reset time from the limit detail onto the window", async () => {
// Live payload shape: `resetTime` lives on `detail`, while `window`
// carries only duration/timeUnit. The 5h row must still render
// "resets in …" in `omp usage`.
const detailReset = "2026-07-18T05:43:35.355947Z";
const usageReset = "2026-07-21T07:43:35.355947Z";
const report = await kimiUsageProvider.fetchUsage!(
{ provider: "kimi-code", credential: makeCredential(), signal: undefined },
makeCtx({
usage: { limit: "100", used: "28", remaining: "72", resetTime: usageReset },
limits: [
{
window: { duration: 300, timeUnit: "TIME_UNIT_MINUTE" },
detail: { limit: "100", remaining: "100", resetTime: detailReset },
},
],
}),
);
expect(report).not.toBeNull();
expect(report!.limits).toHaveLength(2);
const total = report!.limits[0]!;
expect(total.label).toBe("Total quota");
expect(total.window?.resetsAt).toBe(Date.parse(usageReset));
const fiveHour = report!.limits[1]!;
expect(fiveHour.label).toBe("5h limit");
expect(fiveHour.window?.durationMs).toBe(5 * 60 * 60 * 1000);
expect(fiveHour.window?.resetsAt).toBe(Date.parse(detailReset));
});
it("keeps an explicit window resetTime authoritative over the detail one", async () => {
const windowReset = "2026-07-18T06:00:00.000Z";
const detailReset = "2026-07-18T05:43:35.355947Z";
const report = await kimiUsageProvider.fetchUsage!(
{ provider: "kimi-code", credential: makeCredential(), signal: undefined },
makeCtx({
limits: [
{
window: { duration: 300, timeUnit: "TIME_UNIT_MINUTE", resetTime: windowReset },
detail: { limit: "100", remaining: "40", resetTime: detailReset },
},
],
}),
);
expect(report).not.toBeNull();
expect(report!.limits).toHaveLength(1);
expect(report!.limits[0]!.window?.resetsAt).toBe(Date.parse(windowReset));
});
});