@@ -782,10 +784,10 @@ export default function KeyInfoView({
: t("keyDetail.keyInfoView.overview.ofBudget", { budget: budgetDisplay })}
- {currentKeyData.budget_reset_at && (
+ {budgetResetTime && (
{t("keyDetail.keyInfoView.overview.resets")}
- {formatTimestamp(currentKeyData.budget_reset_at)}
+ {budgetResetTime}
)}
@@ -1111,8 +1113,8 @@ export default function KeyInfoView({
{t("keyDetail.keyInfoView.settings.budgetReset")}
- {currentKeyData.budget_reset_at
- ? `${currentKeyData.budget_duration ? t("keyDetail.keyInfoView.settings.budgetResetEvery", { duration: currentKeyData.budget_duration }) : ""}${formatTimestamp(currentKeyData.budget_reset_at)}`
+ {budgetResetTime
+ ? `${currentKeyData.budget_duration ? t("keyDetail.keyInfoView.settings.budgetResetEvery", { duration: currentKeyData.budget_duration }) : ""}${budgetResetTime}`
: t("keyDetail.keyInfoView.settings.never")}
diff --git a/ui/litellm-dashboard/src/utils/budgetUtils.test.ts b/ui/litellm-dashboard/src/utils/budgetUtils.test.ts
index 11339d08bc5..2d05d7140d4 100644
--- a/ui/litellm-dashboard/src/utils/budgetUtils.test.ts
+++ b/ui/litellm-dashboard/src/utils/budgetUtils.test.ts
@@ -1,9 +1,14 @@
-import { afterEach, describe, expect, it } from "vitest";
+import { afterEach, beforeEach, describe, expect, it } from "vitest";
-import { budgetPeriodRange, describeBudgetResetSchedule, formatBudgetReset, formatBudgetResetUtc } from "./budgetUtils";
+import {
+ budgetPeriodRange,
+ describeBudgetResetSchedule,
+ formatBudgetResetTime,
+ formatBudgetResetUtc,
+} from "./budgetUtils";
-const localIso = (year: number, monthIndex: number, day: number, hour = 12): string =>
- new Date(year, monthIndex, day, hour, 0, 0).toISOString();
+const utcIso = (year: number, monthIndex: number, day: number, hour = 12): string =>
+ new Date(Date.UTC(year, monthIndex, day, hour, 0, 0)).toISOString();
const originalTimezone: string | undefined = process.env.TZ;
@@ -18,14 +23,36 @@ afterEach(() => {
describe("formatBudgetResetUtc", () => {
it("formats the reset date in UTC regardless of the local timezone", () => {
process.env.TZ = "America/Los_Angeles";
- const resetAt: string = "2026-10-20T00:00:00Z";
+ expect(formatBudgetResetUtc("2026-10-20T00:00:00Z")).toBe("Oct 20, 2026");
+ });
- expect(formatBudgetResetUtc(resetAt)).toBe("Oct 20, 2026");
- expect(formatBudgetReset(resetAt)).toBe("Oct 19, 2026");
+ it("adds the UTC clock time when the reset is not at midnight UTC", () => {
+ process.env.TZ = "America/Los_Angeles";
+ expect(formatBudgetResetUtc("2026-11-30T18:30:00Z")).toBe("Nov 30, 2026 18:30");
+ });
+});
+
+describe("formatBudgetResetTime", () => {
+ it.each(["America/Los_Angeles", "Asia/Tokyo", "UTC"])(
+ "prints the reset instant in UTC and says so for a %s viewer",
+ (timeZone) => {
+ process.env.TZ = timeZone;
+ expect(formatBudgetResetTime("2026-11-01T00:00:00Z")).toBe("Nov 1, 2026 at 12:00 AM UTC");
+ expect(formatBudgetResetTime("2026-10-31T17:30:00-07:00")).toBe("Nov 1, 2026 at 12:30 AM UTC");
+ expect(formatBudgetResetTime("2026-11-01T13:05:00Z")).toBe("Nov 1, 2026 at 1:05 PM UTC");
+ },
+ );
+
+ it.each([null, undefined, "", "not-a-date"])("returns null for %s", (value) => {
+ expect(formatBudgetResetTime(value)).toBeNull();
});
});
describe("describeBudgetResetSchedule", () => {
+ beforeEach(() => {
+ process.env.TZ = "America/Los_Angeles";
+ });
+
it("reports lifetime spend with no period when the budget has no duration and no scheduled reset", () => {
const schedule = describeBudgetResetSchedule(null, null);
expect(schedule.label).toBe("Never resets");
@@ -41,62 +68,67 @@ describe("describeBudgetResetSchedule", () => {
expect(schedule.tooltip).toContain("every 30d");
});
- it("shows the next reset with its time zone and the rollover carryover note", () => {
- const resetAt = new Date(2026, 6, 15, 12, 0, 0);
- const schedule = describeBudgetResetSchedule("30d", resetAt.toISOString());
- expect(schedule.label).toBe("Jul 15, 2026 12:00");
- expect(schedule.resetAt).toBe(resetAt.toISOString());
- expect(schedule.tooltip).toContain(Intl.DateTimeFormat().resolvedOptions().timeZone);
+ it("shows the next reset in UTC with the rollover carryover note", () => {
+ const resetAt = utcIso(2026, 6, 15);
+ const schedule = describeBudgetResetSchedule("30d", resetAt);
+ expect(schedule.label).toBe("Jul 15, 2026 12:00 UTC");
+ expect(schedule.resetAt).toBe(resetAt);
+ expect(schedule.tooltip).toContain("Next reset Jul 15, 2026 12:00 (UTC).");
expect(schedule.tooltip).toContain("carries into the next period");
});
it("treats 30d as one calendar month back from the reset, like the proxy does", () => {
- const schedule = describeBudgetResetSchedule("30d", localIso(2026, 10, 1, 0));
- expect(schedule.period).toBe("Oct 1 - Nov 1, 2026");
- expect(schedule.tooltip).toContain("covers Oct 1 - Nov 1, 2026 and resets every 30d");
+ const schedule = describeBudgetResetSchedule("30d", utcIso(2026, 10, 1, 0));
+ expect(schedule.period).toBe("Oct 1 - Nov 1, 2026 UTC");
+ expect(schedule.tooltip).toContain("covers Oct 1 - Nov 1, 2026 UTC and resets every 30d");
});
it("subtracts the exact duration for day and week windows", () => {
- expect(describeBudgetResetSchedule("7d", localIso(2026, 6, 15)).period).toBe("Jul 8 - Jul 15, 2026");
- expect(describeBudgetResetSchedule("2w", localIso(2026, 6, 15)).period).toBe("Jul 1 - Jul 15, 2026");
- expect(describeBudgetResetSchedule("weekly", localIso(2026, 6, 15)).period).toBe("Jul 8 - Jul 15, 2026");
+ expect(describeBudgetResetSchedule("7d", utcIso(2026, 6, 15, 0)).period).toBe("Jul 8 - Jul 15, 2026 UTC");
+ expect(describeBudgetResetSchedule("2w", utcIso(2026, 6, 15, 0)).period).toBe("Jul 1 - Jul 15, 2026 UTC");
+ expect(describeBudgetResetSchedule("weekly", utcIso(2026, 6, 15, 0)).period).toBe("Jul 8 - Jul 15, 2026 UTC");
});
- it("walks the month back in UTC so a non-UTC viewer sees the proxy's window", () => {
- const originalTz = process.env.TZ;
+ it("shows the proxy's UTC month window to a viewer whose local date is a day behind", () => {
process.env.TZ = "America/New_York";
- try {
- const schedule = describeBudgetResetSchedule("30d", "2026-03-01T00:00:00Z");
- expect(schedule.period).toBe("Jan 31 - Feb 28, 2026");
- } finally {
- process.env.TZ = originalTz;
- }
+ const schedule = describeBudgetResetSchedule("30d", "2026-03-01T00:00:00Z");
+ expect(schedule.period).toBe("Feb 1 - Mar 1, 2026 UTC");
+ expect(schedule.label).toBe("Mar 1, 2026 00:00 UTC");
});
it("clamps month windows to the last day of a shorter month", () => {
- expect(describeBudgetResetSchedule("1mo", localIso(2026, 2, 31)).period).toBe("Feb 28 - Mar 31, 2026");
+ expect(describeBudgetResetSchedule("1mo", utcIso(2026, 2, 31, 0)).period).toBe("Feb 28 - Mar 31, 2026 UTC");
});
it("spells out both years when the period crosses a year boundary", () => {
- expect(describeBudgetResetSchedule("30d", localIso(2027, 0, 10)).period).toBe("Dec 10, 2026 - Jan 10, 2027");
+ expect(describeBudgetResetSchedule("30d", utcIso(2027, 0, 10, 0)).period).toBe("Dec 10, 2026 - Jan 10, 2027 UTC");
+ });
+
+ it("keeps the clock times when the reset is not at midnight UTC", () => {
+ expect(describeBudgetResetSchedule("1d", "2026-11-08T18:30:00Z").period).toBe(
+ "Nov 7, 2026 18:30 - Nov 8, 2026 18:30 UTC",
+ );
+ expect(describeBudgetResetSchedule("30d", "2026-12-01T09:00:00Z").period).toBe(
+ "Nov 1, 2026 09:00 - Dec 1, 2026 09:00 UTC",
+ );
});
it("keeps both clock times when an hourly window crosses midnight", () => {
- expect(describeBudgetResetSchedule("1h", localIso(2026, 6, 16, 0)).period).toBe(
- "Jul 15, 2026 23:00 - Jul 16, 2026 00:00",
+ expect(describeBudgetResetSchedule("1h", utcIso(2026, 6, 16, 0)).period).toBe(
+ "Jul 15, 2026 23:00 - Jul 16, 2026 00:00 UTC",
);
});
it("shows seconds for a window shorter than a minute", () => {
- expect(describeBudgetResetSchedule("30s", localIso(2026, 6, 15)).period).toBe("Jul 15, 2026 11:59:30 - 12:00:00");
+ expect(describeBudgetResetSchedule("30s", utcIso(2026, 6, 15)).period).toBe("Jul 15, 2026 11:59:30 - 12:00:00 UTC");
});
it("shows clock times for a window that starts and ends on the same day", () => {
- expect(describeBudgetResetSchedule("1h", localIso(2026, 6, 15)).period).toBe("Jul 15, 2026 11:00 - 12:00");
+ expect(describeBudgetResetSchedule("1h", utcIso(2026, 6, 15)).period).toBe("Jul 15, 2026 11:00 - 12:00 UTC");
});
it("falls back to a one day window for a duration the proxy would not recognise", () => {
- expect(describeBudgetResetSchedule("fortnightly", localIso(2026, 6, 15)).period).toBe("Jul 14 - Jul 15, 2026");
+ expect(describeBudgetResetSchedule("fortnightly", utcIso(2026, 6, 15, 0)).period).toBe("Jul 14 - Jul 15, 2026 UTC");
});
it("shows no period when the reset date is unparseable", () => {
@@ -108,7 +140,7 @@ describe("describeBudgetResetSchedule", () => {
it("never describes the window as a cycle", () => {
const schedules = [
- describeBudgetResetSchedule("30d", localIso(2026, 6, 15)),
+ describeBudgetResetSchedule("30d", utcIso(2026, 6, 15)),
describeBudgetResetSchedule("30d", null),
describeBudgetResetSchedule(null, null),
];
diff --git a/ui/litellm-dashboard/src/utils/budgetUtils.ts b/ui/litellm-dashboard/src/utils/budgetUtils.ts
index de144b13dd7..8b87768186d 100644
--- a/ui/litellm-dashboard/src/utils/budgetUtils.ts
+++ b/ui/litellm-dashboard/src/utils/budgetUtils.ts
@@ -6,18 +6,24 @@ import i18n from "@/i18n/i18n";
dayjs.extend(utc);
-export function formatBudgetReset(iso: string | null | undefined): string | null {
+export function formatBudgetResetTime(iso: string | null | undefined, t: TFunction = i18n.t.bind(i18n)): string | null {
if (!iso) return null;
- const resetDate = dayjs(iso);
+ const resetDate = dayjs.utc(iso);
if (!resetDate.isValid()) return null;
- return resetDate.format("MMM D, YYYY");
+ const timestamp = t("keyDetail.keyInfoView.timestamp", {
+ date: resetDate.format("MMM D, YYYY"),
+ time: resetDate.format("h:mm A"),
+ });
+ return `${timestamp} UTC`;
}
+const isMidnight = (instant: Dayjs): boolean => instant.isSame(instant.startOf("day"));
+
export function formatBudgetResetUtc(iso: string | null | undefined): string | null {
if (!iso) return null;
const resetDate = dayjs.utc(iso);
if (!resetDate.isValid()) return null;
- return resetDate.format("MMM D, YYYY");
+ return resetDate.format(isMidnight(resetDate) ? "MMM D, YYYY" : "MMM D, YYYY HH:mm");
}
export interface BudgetResetSchedule {
@@ -62,7 +68,7 @@ export function getBudgetPeriodStart(budgetDuration: string, resetAt: Dayjs): Da
export function formatBudgetPeriod(start: Dayjs, end: Dayjs): string {
const windowSeconds = end.diff(start, "second");
- if (windowSeconds < 24 * 60 * 60) {
+ if (windowSeconds < 24 * 60 * 60 || !isMidnight(start) || !isMidnight(end)) {
const time = windowSeconds < 60 ? "HH:mm:ss" : "HH:mm";
const endFormat = start.isSame(end, "day") ? time : `MMM D, YYYY ${time}`;
return `${start.format(`MMM D, YYYY ${time}`)} - ${end.format(endFormat)}`;
@@ -76,17 +82,21 @@ export function describeBudgetResetSchedule(
budgetResetAt: string | null | undefined,
t: TFunction = i18n.t.bind(i18n),
): BudgetResetSchedule {
- const resetDate = budgetResetAt ? dayjs(budgetResetAt) : null;
+ const resetDate = budgetResetAt ? dayjs.utc(budgetResetAt) : null;
if (resetDate?.isValid()) {
- const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
- const label = resetDate.format("MMM D, YYYY HH:mm");
+ const label = resetDate.format("MMM D, YYYY HH:mm [UTC]");
const period = budgetDuration
- ? formatBudgetPeriod(getBudgetPeriodStart(budgetDuration, resetDate.utc()).local(), resetDate)
+ ? `${formatBudgetPeriod(getBudgetPeriodStart(budgetDuration, resetDate), resetDate)} UTC`
: null;
const coverage = period
? t("budgetSchedule.coverageWithPeriod", { period, duration: budgetDuration })
: t("budgetSchedule.coverageWithoutPeriod");
- const tooltipValues = { coverage, label, timeZone, rolloverNote: t("budgetSchedule.rolloverNote") };
+ const tooltipValues = {
+ coverage,
+ label: resetDate.format("MMM D, YYYY HH:mm"),
+ timeZone: "UTC",
+ rolloverNote: t("budgetSchedule.rolloverNote"),
+ };
return {
label,
resetAt: resetDate.toISOString(),