Merge PR #8197: fix(status-line): recognized kimi-code subscription windows in the usage segment (@tehfiend)

This commit is contained in:
can1357
2026-08-11 15:06:16 +02:00
4 changed files with 152 additions and 8 deletions
+29 -5
View File
@@ -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<string, unknown>, 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<string, unknown>, 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)) {
+31
View File
@@ -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");
});
});
@@ -1407,7 +1407,7 @@ export class StatusLineComponent implements Component {
const l = limit as {
id?: string;
scope?: { windowId?: string; tier?: string };
window?: { resetsAt?: number };
window?: { resetsAt?: number; durationMs?: number };
amount?: { usedFraction?: number };
};
const fraction = l.amount?.usedFraction;
@@ -1415,9 +1415,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:
@@ -1425,7 +1438,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:
@@ -430,4 +430,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");
});
});