Merge PR #8197: fix(status-line): recognized kimi-code subscription windows in the usage segment (@tehfiend)
This commit is contained in:
@@ -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)) {
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user