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:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user