Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 6 additions & 14 deletions src/server/management/logs-usage-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import {
type PersistedUsageEntry,
} from "../../usage/log";
import { getUsageDebugLogEntries } from "../../usage/debug";
import { parseRange, parseUsageSurface, summarizeUsage, type UsageRange, type UsageSummary, type UsageSurface } from "../../usage/summary";
import { parseRange, parseUsageSurface, rangeWindow, summarizeUsage, type UsageRange, type UsageSummary, type UsageSurface } from "../../usage/summary";
import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
import { getProviderRegistryEntry } from "../../providers/registry";
import { getDebugLogEntries } from "../../lib/debug-log-buffer";
Expand Down Expand Up @@ -102,24 +102,16 @@ function nextLocalMidnight(now: number): number {
}

function usageSummaryExpiresAt(
entries: PersistedUsageEntry[],
range: UsageRange,
surface: UsageSurface,
_entries: PersistedUsageEntry[],
_range: UsageRange,
_surface: UsageSurface,
now: number,
): number {
let expiresAt = nextLocalMidnight(now);
const windowMs = range === "7d" ? 7 * USAGE_DAY_MS : range === "30d" ? 30 * USAGE_DAY_MS : null;
if (windowMs === null) return expiresAt;
for (const entry of entries) {
if (!usageEntryMatchesSurface(entry, surface)) continue;
const expiry = entry.timestamp + windowMs;
if (expiry > now && expiry < expiresAt) expiresAt = expiry;
}
return expiresAt;
return nextLocalMidnight(now);
}

function refreshedUsageSummary<T extends UsageSummary & { historyTruncated: boolean }>(summary: T, range: UsageRange, now: number): T {
const since = range === "7d" ? now - 7 * USAGE_DAY_MS : range === "30d" ? now - 30 * USAGE_DAY_MS : null;
const { since } = rangeWindow(range, now);
return { ...summary, since, generatedAt: now };
}

Expand Down
25 changes: 21 additions & 4 deletions src/usage/summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,23 @@ export function parseUsageSurface(input: string | null | undefined): UsageSurfac
return "all";
}

function rangeWindow(range: UsageRange, now: number): { since: number | null; days: number } {
if (range === "7d") return { since: now - 7 * DAY_MS, days: 7 };
if (range === "30d") return { since: now - 30 * DAY_MS, days: 30 };
function startOfLocalDay(ts: number): number {
const d = new Date(ts);
d.setHours(0, 0, 0, 0);
return d.getTime();
}

export function rangeWindow(range: UsageRange, now: number): { since: number | null; days: number } {
if (range === "7d") {
const start = new Date(startOfLocalDay(now));
start.setDate(start.getDate() - 6);
return { since: start.getTime(), days: 7 };
}
if (range === "30d") {
const start = new Date(startOfLocalDay(now));
start.setDate(start.getDate() - 29);
return { since: start.getTime(), days: 30 };
}
return { since: null, days: 0 };
}

Expand Down Expand Up @@ -347,8 +361,11 @@ function buildDayGrid(range: UsageRange, since: number | null, now: number, entr
m.attemptCount += 1;
m.totalTokens += usageDisplayTotalTokens(attribution.usage, attribution.totalTokens) ?? 0;
};
const startOfToday = startOfLocalDay(now);
for (let i = days - 1; i >= 0; i--) {
const key = localDateKey(now - i * DAY_MS);
const d = new Date(startOfToday);
d.setDate(d.getDate() - i);
const key = localDateKey(d.getTime());
grid.set(key, { date: key, requests: 0, measuredRequests: 0, reportedRequests: 0, totalTokens: 0, models: [] });
}
for (const entry of entries) {
Expand Down
69 changes: 69 additions & 0 deletions tests/usage-summary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -852,4 +852,73 @@ describe("summarizeUsage", () => {
});
});

test("7d and 30d range windows align to calendar day boundaries (00:00:00) so completed days remain stable (#1580)", () => {
// Construct local midnight for 2026-08-13
const todayMidnight = new Date(2026, 7, 13, 0, 0, 0, 0).getTime();
const dayMs = 86_400_000;

// Day -29 (2026-07-15) at 04:00 AM (for 30d boundary)
const dayMinus29Date = new Date(todayMidnight);
dayMinus29Date.setDate(dayMinus29Date.getDate() - 29);
dayMinus29Date.setHours(4, 0, 0, 0);
const dayMinus29Ts = dayMinus29Date.getTime();

// Day -6 (2026-08-07) at 02:30 AM (for 7d boundary)
const dayMinus6Early = todayMidnight - 6 * dayMs + 2.5 * 3600_000;
// Yesterday (2026-08-12) at 03:00 AM
const yesterdayEarly = todayMidnight - 1 * dayMs + 3 * 3600_000;
// Yesterday (2026-08-12) at 19:00 PM
const yesterdayLate = todayMidnight - 1 * dayMs + 19 * 3600_000;
// Today (2026-08-13) at 08:00 AM
const todayMorning = todayMidnight + 8 * 3600_000;

const entries: PersistedUsageEntry[] = [
entry({ ts: dayMinus29Ts, usageStatus: "reported", usage: { inputTokens: 600, outputTokens: 600 }, totalTokens: 1200 }),
entry({ ts: dayMinus6Early, usageStatus: "reported", usage: { inputTokens: 250, outputTokens: 250 }, totalTokens: 500 }),
entry({ ts: yesterdayEarly, usageStatus: "reported", usage: { inputTokens: 400, outputTokens: 400 }, totalTokens: 800 }),
entry({ ts: yesterdayLate, usageStatus: "reported", usage: { inputTokens: 100, outputTokens: 100 }, totalTokens: 200 }),
entry({ ts: todayMorning, usageStatus: "reported", usage: { inputTokens: 150, outputTokens: 150 }, totalTokens: 300 }),
];

// Summary at 09:00 AM today (7d)
const sumMorning7d = summarizeUsage(entries, "7d", todayMidnight + 9 * 3600_000);
const day6Morning = sumMorning7d.days.find(d => d.date.endsWith("08-07"))?.totalTokens;
const yesterdayMorningTotal = sumMorning7d.days.find(d => d.date.endsWith("08-12"))?.totalTokens;

expect(day6Morning).toBe(500);
expect(yesterdayMorningTotal).toBe(1000);

// Summary at 09:00 AM today (30d)
const sumMorning30d = summarizeUsage(entries, "30d", todayMidnight + 9 * 3600_000);
const day29Morning = sumMorning30d.days.find(d => d.date.endsWith("07-15"))?.totalTokens;
expect(sumMorning30d.days).toHaveLength(30);
expect(day29Morning).toBe(1200);

// Summary at 23:30 PM today (later in the day) with an additional turn today
const todayEvening = todayMidnight + 20 * 3600_000;
const entriesLater = [
...entries,
entry({ ts: todayEvening, usageStatus: "reported", usage: { inputTokens: 50, outputTokens: 50 }, totalTokens: 100 }),
];

// 7d evening
const sumEvening7d = summarizeUsage(entriesLater, "7d", todayMidnight + 23.5 * 3600_000);
const day6Evening = sumEvening7d.days.find(d => d.date.endsWith("08-07"))?.totalTokens;
const yesterdayEveningTotal = sumEvening7d.days.find(d => d.date.endsWith("08-12"))?.totalTokens;
const todayEveningTotal = sumEvening7d.days.find(d => d.date.endsWith("08-13"))?.totalTokens;

// Critical assertion: completed days NEVER lose tokens as hours progress in 7d
expect(day6Evening).toBe(500);
expect(yesterdayEveningTotal).toBe(1000);
expect(todayEveningTotal).toBe(400); // 300 + 100
expect(sumMorning7d.since).toBe(sumEvening7d.since);

// 30d evening
const sumEvening30d = summarizeUsage(entriesLater, "30d", todayMidnight + 23.5 * 3600_000);
const day29Evening = sumEvening30d.days.find(d => d.date.endsWith("07-15"))?.totalTokens;
expect(sumEvening30d.days).toHaveLength(30);
expect(day29Evening).toBe(1200);
expect(sumMorning30d.since).toBe(sumEvening30d.since);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

});
Loading