From e7f487381b6108ae215893035ee2cdb2f17782c8 Mon Sep 17 00:00:00 2001 From: Bejay Cole Date: Mon, 10 Aug 2026 15:36:05 -0600 Subject: [PATCH] fix(status-line): recognized kimi-code subscription windows in the usage segment Kimi's usage provider built window ids from the raw duration/timeUnit ("300time_unit_minute") and left the aggregate quota on "default", so the status-line usage segment, which matches canonical "5h"/"7d" windows, never rendered for kimi-code sessions. Derive window ids from the reported span instead (300 minutes -> "5h", 7 days -> "7d"), mirroring the minimax-code convention, and tag the aggregate quota as the weekly window. The payload carries only resetTime for the aggregate, but the observed reset horizon matches the plan's weekly cycle. The segment aggregator also falls back to the reported span (duration within a minute of 5h or 7d) when the id isn't canonical, so cache rows written by older builds still light up the 5h meter in mixed-version fleets. --- packages/ai/src/usage/kimi.ts | 34 +++++++-- packages/ai/test/kimi-usage.test.ts | 31 ++++++++ .../modes/components/status-line/component.ts | 19 ++++- .../test/status-line-usage.test.ts | 76 +++++++++++++++++++ 4 files changed, 152 insertions(+), 8 deletions(-) diff --git a/packages/ai/src/usage/kimi.ts b/packages/ai/src/usage/kimi.ts index 3cb021920..94d2a4bd8 100644 --- a/packages/ai/src/usage/kimi.ts +++ b/packages/ai/src/usage/kimi.ts @@ -77,6 +77,23 @@ function formatDurationLabel(duration: number, timeUnit: string): string | undef return undefined; } +const MINUTE_MS = 60_000; +const HOUR_MS = 3_600_000; +const DAY_MS = 86_400_000; + +/** + * Status-line and ranking consumers match on canonical window ids ("5h", + * "7d"), so derive the id from the reported span: the 300-minute burst window + * surfaces as "5h" instead of "300time_unit_minute". Mirrors the + * intervalWindowId convention in minimax-code.ts. + */ +function canonicalWindowId(durationMs: number): string { + if (durationMs > 0 && durationMs % DAY_MS === 0) return `${durationMs / DAY_MS}d`; + if (durationMs > 0 && durationMs % HOUR_MS === 0) return `${durationMs / HOUR_MS}h`; + const minutes = Math.round(durationMs / MINUTE_MS); + return minutes > 0 ? `${minutes}m` : "default"; +} + function buildWindow(windowData: Record, nowMs: number): UsageWindow | undefined { const duration = toNumber(windowData.duration); const timeUnit = typeof windowData.timeUnit === "string" ? windowData.timeUnit : ""; @@ -86,14 +103,15 @@ function buildWindow(windowData: Record, nowMs: number): UsageW if (duration === undefined && !label && !resetsAt) return undefined; let durationMs: number | undefined; if (duration !== undefined) { - if (timeUnit.toUpperCase().includes("MINUTE")) durationMs = duration * 60_000; - else if (timeUnit.toUpperCase().includes("HOUR")) durationMs = duration * 3_600_000; - else if (timeUnit.toUpperCase().includes("DAY")) durationMs = duration * 86_400_000; + if (timeUnit.toUpperCase().includes("MINUTE")) durationMs = duration * MINUTE_MS; + else if (timeUnit.toUpperCase().includes("HOUR")) durationMs = duration * HOUR_MS; + else if (timeUnit.toUpperCase().includes("DAY")) durationMs = duration * DAY_MS; + else if (timeUnit.toUpperCase().includes("WEEK")) durationMs = duration * 7 * DAY_MS; else if (timeUnit.toUpperCase().includes("SECOND")) durationMs = duration * 1000; } return { - id: duration !== undefined && timeUnit ? `${duration}${timeUnit.toLowerCase()}` : "default", + id: durationMs !== undefined ? canonicalWindowId(durationMs) : "default", label: label ?? "Usage window", durationMs, resetsAt, @@ -177,7 +195,13 @@ function parseUsagePayload(payload: unknown, nowMs: number): { rows: KimiUsageRo if (isRecord(data.usage)) { const summary = buildUsageRow(data.usage, "Total quota", nowMs); - if (summary) rows.push(summary); + if (summary) { + // Kimi Code's aggregate quota resets weekly, but the payload carries + // only `resetTime` and no duration. Attach the canonical weekly + // window explicitly so status-line/ranking consumers recognize it. + summary.window = { id: "7d", label: "7 Day", resetsAt: summary.resetsAt }; + rows.push(summary); + } } if (Array.isArray(data.limits)) { diff --git a/packages/ai/test/kimi-usage.test.ts b/packages/ai/test/kimi-usage.test.ts index b86b66310..559452980 100644 --- a/packages/ai/test/kimi-usage.test.ts +++ b/packages/ai/test/kimi-usage.test.ts @@ -45,11 +45,19 @@ describe("kimi usage provider", () => { const total = report!.limits[0]!; expect(total.label).toBe("Total quota"); expect(total.window?.resetsAt).toBe(Date.parse(usageReset)); + // The aggregate quota is the weekly subscription window; canonical id + // lets the status-line usage segment pick it up. + expect(total.window?.id).toBe("7d"); + expect(total.scope?.windowId).toBe("7d"); 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)); + // 300 minutes canonicalizes to "5h" so the status-line usage segment + // recognizes the burst window. + expect(fiveHour.window?.id).toBe("5h"); + expect(fiveHour.scope?.windowId).toBe("5h"); }); it("keeps an explicit window resetTime authoritative over the detail one", async () => { @@ -71,4 +79,27 @@ describe("kimi usage provider", () => { expect(report!.limits).toHaveLength(1); expect(report!.limits[0]!.window?.resetsAt).toBe(Date.parse(windowReset)); }); + + it("canonicalizes whole-day and non-standard window durations", async () => { + const report = await kimiUsageProvider.fetchUsage!( + { provider: "kimi-code", credential: makeCredential(), signal: undefined }, + makeCtx({ + limits: [ + { + window: { duration: 7, timeUnit: "TIME_UNIT_DAY" }, + detail: { limit: "100", remaining: "50" }, + }, + { + window: { duration: 90, timeUnit: "TIME_UNIT_MINUTE" }, + detail: { limit: "100", remaining: "50" }, + }, + ], + }), + ); + + expect(report).not.toBeNull(); + expect(report!.limits).toHaveLength(2); + expect(report!.limits[0]!.window?.id).toBe("7d"); + expect(report!.limits[1]!.window?.id).toBe("90m"); + }); }); diff --git a/packages/coding-agent/src/modes/components/status-line/component.ts b/packages/coding-agent/src/modes/components/status-line/component.ts index e423de910..025063d5a 100644 --- a/packages/coding-agent/src/modes/components/status-line/component.ts +++ b/packages/coding-agent/src/modes/components/status-line/component.ts @@ -1389,7 +1389,7 @@ export class StatusLineComponent implements Component { } const l = limit as { scope?: { windowId?: string; tier?: string }; - window?: { resetsAt?: number }; + window?: { resetsAt?: number; durationMs?: number }; amount?: { usedFraction?: number }; }; const fraction = l.amount?.usedFraction; @@ -1397,9 +1397,22 @@ export class StatusLineComponent implements Component { const windowId = l.scope?.windowId; const tier = l.scope?.tier; const resetsAt = l.window?.resetsAt; + // Canonical window ids win. Fall back to the reported span (same + // tolerance as the 5h priority-boost check) so providers that emit + // non-canonical ids, and cache rows written before a provider was + // canonicalized, still map onto the two subscription windows. + const durationMs = l.window?.durationMs; + const windowClass = + windowId === "5h" || windowId === "7d" + ? windowId + : durationMs !== undefined && Math.abs(durationMs - 5 * 3_600_000) <= 60_000 + ? "5h" + : durationMs !== undefined && Math.abs(durationMs - 7 * 86_400_000) <= 60_000 + ? "7d" + : undefined; // Accept tiered limits, but prefer untiered (backward compat with Anthropic). // An untiered limit always replaces a tiered one; among same-tieredness, first wins. - if (windowId === "5h" && (!fiveHour || (fiveHourTier !== undefined && !tier))) { + if (windowClass === "5h" && (!fiveHour || (fiveHourTier !== undefined && !tier))) { fiveHour = { percent: fraction * 100, resetMinutes: @@ -1407,7 +1420,7 @@ export class StatusLineComponent implements Component { }; fiveHourTier = tier || undefined; } - if (windowId === "7d" && (!sevenDay || (sevenDayTier !== undefined && !tier))) { + if (windowClass === "7d" && (!sevenDay || (sevenDayTier !== undefined && !tier))) { sevenDay = { percent: fraction * 100, resetHours: diff --git a/packages/coding-agent/test/status-line-usage.test.ts b/packages/coding-agent/test/status-line-usage.test.ts index d596312d4..f7504d82d 100644 --- a/packages/coding-agent/test/status-line-usage.test.ts +++ b/packages/coding-agent/test/status-line-usage.test.ts @@ -338,4 +338,80 @@ describe("usage status-line segment", () => { expect(stripVTControlCharacters(highWithoutValue)).toBe(stripVTControlCharacters(lowWithoutValue)); expect(highWithoutValue).not.toBe(lowWithoutValue); }); + + it("maps non-canonical window ids onto subscription windows by reported span", async () => { + // Kimi-shaped rows: the burst window reports duration/timeUnit instead + // of a canonical id, and rows written before canonicalization keep the + // old id. The reported span still identifies the window. + const now = Date.now(); + const component = makeComponent([ + { + limits: [ + { + scope: { windowId: "300time_unit_minute" }, + window: { durationMs: 5 * 3_600_000, resetsAt: now + 30 * 60_000 }, + amount: { usedFraction: 0.24 }, + }, + { + scope: { windowId: "weekly" }, + window: { durationMs: 7 * 86_400_000, resetsAt: now + 141 * 3_600_000 }, + amount: { usedFraction: 0.08 }, + }, + ], + }, + ]); + + component.refreshUsageInBackground(); + await flushUsageRefresh(); + const content = stripVTControlCharacters(component.getTopBorder(200).content); + + expect(content).toContain("5h"); + expect(content).toContain("24%"); + expect(content).toContain("7d"); + expect(content).toContain("8%"); + }); + + it("ignores non-canonical windows without a reported span", async () => { + const component = makeComponent([ + { + limits: [ + { scope: { windowId: "default" }, window: {}, amount: { usedFraction: 0.24 } }, + { + scope: { windowId: "monthly" }, + window: { durationMs: 30 * 86_400_000 }, + amount: { usedFraction: 0.5 }, + }, + ], + }, + ]); + + component.refreshUsageInBackground(); + await flushUsageRefresh(); + const content = stripVTControlCharacters(component.getTopBorder(200).content); + + expect(content).not.toContain("24%"); + expect(content).not.toContain("50%"); + }); + + it("prefers canonical window ids over a conflicting reported span", async () => { + const component = makeComponent([ + { + limits: [ + { + scope: { windowId: "5h" }, + window: { durationMs: 7 * 86_400_000 }, + amount: { usedFraction: 0.24 }, + }, + ], + }, + ]); + + component.refreshUsageInBackground(); + await flushUsageRefresh(); + const content = stripVTControlCharacters(component.getTopBorder(200).content); + + expect(content).toContain("5h"); + expect(content).toContain("24%"); + expect(content).not.toContain("7d"); + }); });