diff --git a/ui/litellm-dashboard/src/components/team/MyUserTab.test.tsx b/ui/litellm-dashboard/src/components/team/MyUserTab.test.tsx
index 24ce626ecb7..eee9fcdadd1 100644
--- a/ui/litellm-dashboard/src/components/team/MyUserTab.test.tsx
+++ b/ui/litellm-dashboard/src/components/team/MyUserTab.test.tsx
@@ -48,7 +48,7 @@ describe("MyUserTab", () => {
expect(screen.getByText("Never resets")).toBeInTheDocument();
});
- it("shows the next reset time when the member budget has a reset window", () => {
+ it("shows the budget period and next reset when the member budget has a reset window", () => {
vi.mocked(useMyTeamMember).mockReturnValue({
data: {
user_id: "user-1",
@@ -69,6 +69,8 @@ describe("MyUserTab", () => {
renderWithProviders();
- expect(screen.getByText("Resets Jul 15, 2026 12:00")).toBeInTheDocument();
+ expect(screen.getByText("Current Period Spend (USD)")).toBeInTheDocument();
+ expect(screen.getByText("Jun 15 - Jul 15, 2026 (resets Jul 15, 2026 12:00)")).toBeInTheDocument();
+ expect(screen.queryByText(/cycle/i)).not.toBeInTheDocument();
});
});
diff --git a/ui/litellm-dashboard/src/components/team/MyUserTab.tsx b/ui/litellm-dashboard/src/components/team/MyUserTab.tsx
index bf1a2c2c242..8d7583c51e4 100644
--- a/ui/litellm-dashboard/src/components/team/MyUserTab.tsx
+++ b/ui/litellm-dashboard/src/components/team/MyUserTab.tsx
@@ -1,4 +1,4 @@
-import { describeBudgetResetSchedule } from "@/utils/budgetUtils";
+import { BudgetResetSchedule, describeBudgetResetSchedule } from "@/utils/budgetUtils";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { SimpleTooltip } from "@/components/ui/tooltip";
import { Badge } from "@/components/ui/badge";
@@ -30,6 +30,12 @@ const formatRateLimit = (value: number | null | undefined): string => {
return formatNumberWithCommas(value, 0);
};
+function describePeriodLine(schedule: BudgetResetSchedule): string {
+ if (schedule.period) return `${schedule.period} (resets ${schedule.label})`;
+ if (schedule.resetAt) return `Resets ${schedule.label}`;
+ return schedule.label;
+}
+
export default function MyUserTab({ teamId }: MyUserTabProps) {
const { data, isLoading, error } = useMyTeamMember(teamId);
@@ -93,16 +99,14 @@ export default function MyUserTab({ teamId }: MyUserTabProps) {
- {labelWithTooltip("Current Cycle Spend (USD)", budgetReset.tooltip)}
+ {labelWithTooltip("Current Period Spend (USD)", budgetReset.tooltip)}
${formatNumber(spend, 4)}
of {maxBudget === null ? "Unlimited" : `$${formatNumber(maxBudget, 4)}`}
-
- {budgetReset.resetAt ? `Resets ${budgetReset.label}` : budgetReset.label}
-
+ {describePeriodLine(budgetReset)}
@@ -121,7 +125,7 @@ export default function MyUserTab({ teamId }: MyUserTabProps) {
{labelWithTooltip(
"Total Spend (USD)",
- "Cumulative spend across budget cycles within this team. This is a lower bound: lifetime tracking began 2026-04-21, so cycles completed before then are not included. It is never less than Current Cycle Spend.",
+ "Cumulative spend within this team across budget periods. This is a lower bound: lifetime tracking began 2026-04-21, so periods that reset before then are not included. It is never less than Current Period Spend.",
)}
${formatNumber(totalSpend, 4)}
diff --git a/ui/litellm-dashboard/src/components/team/TeamMemberTab.test.tsx b/ui/litellm-dashboard/src/components/team/TeamMemberTab.test.tsx
index 38a8e4f97d5..9350de8a8fc 100644
--- a/ui/litellm-dashboard/src/components/team/TeamMemberTab.test.tsx
+++ b/ui/litellm-dashboard/src/components/team/TeamMemberTab.test.tsx
@@ -316,6 +316,29 @@ describe("TeamMembersComponent", () => {
expect(screen.getByText("Jul 15, 2026 12:00")).toBeInTheDocument();
});
+ it("shows the budget period date range instead of a bare reset date when the duration is known", () => {
+ const teamData = createMockTeamData();
+ teamData.team_memberships[0].litellm_budget_table!.budget_duration = "30d";
+ renderWithProviders(
+
,
+ );
+
+ expect(screen.getByRole("columnheader", { name: /^Budget Period/ })).toBeInTheDocument();
+ expect(screen.getByRole("columnheader", { name: /^Current Period Spend \(USD\)/ })).toBeInTheDocument();
+ expect(screen.getByText("Jun 15 - Jul 15, 2026")).toBeInTheDocument();
+ expect(screen.queryByText("Jul 15, 2026 12:00")).not.toBeInTheDocument();
+ expect(screen.queryByText(/cycle/i)).not.toBeInTheDocument();
+ });
+
it("shows Never resets for a member whose budget has no reset window", () => {
const teamData = createMockTeamData();
teamData.team_memberships[0].litellm_budget_table!.budget_reset_at = null;
@@ -696,7 +719,7 @@ describe("TeamMembersComponent", () => {
/>,
);
- it("resets the member's current cycle spend to $0 after confirming, then refreshes the team", async () => {
+ it("resets the member's current period spend to $0 after confirming, then refreshes the team", async () => {
const user = userEvent.setup();
POST.mockResolvedValue({ data: {} });
renderEditableTab();
@@ -745,7 +768,7 @@ describe("TeamMembersComponent", () => {
expect(POST).not.toHaveBeenCalled();
});
- it("disables the reset with a reason on members that have no current cycle spend", async () => {
+ it("disables the reset with a reason on members that have no current period spend", async () => {
const user = userEvent.setup();
renderEditableTab();
@@ -753,7 +776,7 @@ describe("TeamMembersComponent", () => {
"reset-member-spend",
);
await user.hover(resetButton);
- expect(await screen.findByText("No current cycle spend to reset")).toBeInTheDocument();
+ expect(await screen.findByText("No current period spend to reset")).toBeInTheDocument();
await user.click(resetButton);
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
diff --git a/ui/litellm-dashboard/src/components/team/TeamMemberTab.tsx b/ui/litellm-dashboard/src/components/team/TeamMemberTab.tsx
index 319f8c973f6..1ff77f565a5 100644
--- a/ui/litellm-dashboard/src/components/team/TeamMemberTab.tsx
+++ b/ui/litellm-dashboard/src/components/team/TeamMemberTab.tsx
@@ -102,7 +102,7 @@ export default function TeamMemberTab({
return "0";
};
- const getUserCurrentCycleSpend = (userId: string | null): number => {
+ const getUserCurrentPeriodSpend = (userId: string | null): number => {
if (!userId) return 0;
return findMembership(userId)?.spend ?? 0;
};
@@ -153,12 +153,16 @@ export default function TeamMemberTab({
return findMembership(userId)?.litellm_budget_table?.budget_reset_at ?? null;
};
- const renderBudgetReset = (userId: string | null) => {
+ const getUserBudgetSchedule = (userId: string | null) => {
const budget = findMembership(userId)?.litellm_budget_table;
- const schedule = describeBudgetResetSchedule(budget?.budget_duration ?? null, budget?.budget_reset_at ?? null);
+ return describeBudgetResetSchedule(budget?.budget_duration ?? null, budget?.budget_reset_at ?? null);
+ };
+
+ const renderBudgetPeriod = (userId: string | null) => {
+ const schedule = getUserBudgetSchedule(userId);
return (
- {schedule.label}
+ {schedule.period ?? schedule.label}
);
};
@@ -200,22 +204,22 @@ export default function TeamMemberTab({
{
title: (
- Current Cycle Spend (USD)
-
-
+ Current Period Spend (USD)
+
+
),
key: "spend",
numeric: true,
- sortValue: (record: Member) => getUserCurrentCycleSpend(record.user_id),
- render: (record: Member) =>
,
+ sortValue: (record: Member) => getUserCurrentPeriodSpend(record.user_id),
+ render: (record: Member) =>
,
},
{
title: (
Total Spend (USD)
-
+
@@ -263,10 +267,10 @@ export default function TeamMemberTab({
},
},
{
- title: "Budget Reset",
+ title: "Budget Period",
key: "budget_reset",
sortValue: (record: Member) => getUserBudgetReset(record.user_id),
- render: (record: Member) => renderBudgetReset(record.user_id),
+ render: (record: Member) => renderBudgetPeriod(record.user_id),
},
{
title: (
@@ -314,10 +318,12 @@ export default function TeamMemberTab({
const resetSpendDisabledReason = (member: Member): string | null => {
if (!isProxyAdmin && member.user_id === userId) return "Ask a proxy admin to reset your own spend";
- if (getUserCurrentCycleSpend(member.user_id) <= 0) return "No current cycle spend to reset";
+ if (getUserCurrentPeriodSpend(member.user_id) <= 0) return "No current period spend to reset";
return null;
};
+ const resetSpendPeriod = getUserBudgetSchedule(memberToResetSpend?.user_id ?? null).period;
+
return (
<>
Reset Team Member Spend
- Reset current cycle spend for{" "}
+ Reset current period spend for{" "}
{memberToResetSpend?.user_email || memberToResetSpend?.user_id} in this team to{" "}
$0?
- Current cycle spend:{" "}
- ${formatNumberWithCommas(getUserCurrentCycleSpend(memberToResetSpend?.user_id ?? null), 4)}
+ Current period spend
+ {resetSpendPeriod ? ` (${resetSpendPeriod})` : ""}:{" "}
+
+ ${formatNumberWithCommas(getUserCurrentPeriodSpend(memberToResetSpend?.user_id ?? null), 4)}
+
. This is the value checked against the member's budget. Total spend and logs are preserved.
diff --git a/ui/litellm-dashboard/src/utils/budgetUtils.test.ts b/ui/litellm-dashboard/src/utils/budgetUtils.test.ts
index e33c779bb65..11339d08bc5 100644
--- a/ui/litellm-dashboard/src/utils/budgetUtils.test.ts
+++ b/ui/litellm-dashboard/src/utils/budgetUtils.test.ts
@@ -2,6 +2,9 @@ import { afterEach, describe, expect, it } from "vitest";
import { budgetPeriodRange, describeBudgetResetSchedule, formatBudgetReset, formatBudgetResetUtc } from "./budgetUtils";
+const localIso = (year: number, monthIndex: number, day: number, hour = 12): string =>
+ new Date(year, monthIndex, day, hour, 0, 0).toISOString();
+
const originalTimezone: string | undefined = process.env.TZ;
afterEach(() => {
@@ -23,15 +26,18 @@ describe("formatBudgetResetUtc", () => {
});
describe("describeBudgetResetSchedule", () => {
- it("reports Never resets when the budget has no duration and no scheduled reset", () => {
+ 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");
expect(schedule.resetAt).toBeNull();
+ expect(schedule.period).toBeNull();
+ expect(schedule.tooltip).toContain("lifetime spend");
});
it("reports Unknown when a duration is set but no reset has been scheduled yet", () => {
const schedule = describeBudgetResetSchedule("30d", null);
expect(schedule.label).toBe("Unknown");
+ expect(schedule.period).toBeNull();
expect(schedule.tooltip).toContain("every 30d");
});
@@ -41,7 +47,74 @@ describe("describeBudgetResetSchedule", () => {
expect(schedule.label).toBe("Jul 15, 2026 12:00");
expect(schedule.resetAt).toBe(resetAt.toISOString());
expect(schedule.tooltip).toContain(Intl.DateTimeFormat().resolvedOptions().timeZone);
- expect(schedule.tooltip).toContain("carries into the next cycle");
+ 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");
+ });
+
+ 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");
+ });
+
+ it("walks the month back in UTC so a non-UTC viewer sees the proxy's window", () => {
+ const originalTz = process.env.TZ;
+ 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;
+ }
+ });
+
+ 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");
+ });
+
+ 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");
+ });
+
+ 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",
+ );
+ });
+
+ 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");
+ });
+
+ 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");
+ });
+
+ 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");
+ });
+
+ it("shows no period when the reset date is unparseable", () => {
+ const schedule = describeBudgetResetSchedule("30d", "not-a-date");
+ expect(schedule.label).toBe("Unknown");
+ expect(schedule.period).toBeNull();
+ expect(schedule.tooltip).not.toMatch(/NaN|Invalid/);
+ });
+
+ it("never describes the window as a cycle", () => {
+ const schedules = [
+ describeBudgetResetSchedule("30d", localIso(2026, 6, 15)),
+ describeBudgetResetSchedule("30d", null),
+ describeBudgetResetSchedule(null, null),
+ ];
+ for (const schedule of schedules) {
+ expect(`${schedule.label} ${schedule.tooltip}`).not.toMatch(/cycle/i);
+ }
});
});
diff --git a/ui/litellm-dashboard/src/utils/budgetUtils.ts b/ui/litellm-dashboard/src/utils/budgetUtils.ts
index 23af15eb539..71638551f9f 100644
--- a/ui/litellm-dashboard/src/utils/budgetUtils.ts
+++ b/ui/litellm-dashboard/src/utils/budgetUtils.ts
@@ -1,4 +1,4 @@
-import dayjs from "dayjs";
+import dayjs, { Dayjs } from "dayjs";
import utc from "dayjs/plugin/utc";
dayjs.extend(utc);
@@ -20,10 +20,55 @@ export function formatBudgetResetUtc(iso: string | null | undefined): string | n
export interface BudgetResetSchedule {
label: string;
resetAt: string | null;
+ period: string | null;
tooltip: string;
}
-const ROLLOVER_NOTE = "Spend above a rollover cap at reset carries into the next cycle.";
+const ROLLOVER_NOTE = "Spend above a rollover cap at reset carries into the next period.";
+
+const DURATION_WORD_ALIASES: Record = {
+ hourly: "1h",
+ daily: "24h",
+ weekly: "7d",
+ monthly: "30d",
+};
+
+const DURATION_PATTERN = /^(\d+)(mo|[smhdw])$/;
+
+// Mirrors the proxy's get_budget_window_start: "30d" and "Nmo" span calendar months,
+// everything else is a fixed length, and an unrecognised duration resets at the next midnight.
+export function getBudgetPeriodStart(budgetDuration: string, resetAt: Dayjs): Dayjs {
+ const trimmed = budgetDuration.trim();
+ const normalized = DURATION_WORD_ALIASES[trimmed.toLowerCase()] ?? trimmed;
+ const match = DURATION_PATTERN.exec(normalized);
+ if (!match) return resetAt.subtract(1, "day");
+ const value = Number(match[1]);
+ switch (match[2]) {
+ case "mo":
+ return resetAt.subtract(value, "month");
+ case "d":
+ return value === 30 ? resetAt.subtract(1, "month") : resetAt.subtract(value, "day");
+ case "w":
+ return resetAt.subtract(value, "week");
+ case "h":
+ return resetAt.subtract(value, "hour");
+ case "m":
+ return resetAt.subtract(value, "minute");
+ default:
+ return resetAt.subtract(value, "second");
+ }
+}
+
+export function formatBudgetPeriod(start: Dayjs, end: Dayjs): string {
+ const windowSeconds = end.diff(start, "second");
+ if (windowSeconds < 24 * 60 * 60) {
+ 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)}`;
+ }
+ if (start.isSame(end, "year")) return `${start.format("MMM D")} - ${end.format("MMM D, YYYY")}`;
+ return `${start.format("MMM D, YYYY")} - ${end.format("MMM D, YYYY")}`;
+}
export function describeBudgetResetSchedule(
budgetDuration: string | null | undefined,
@@ -31,26 +76,33 @@ export function describeBudgetResetSchedule(
): BudgetResetSchedule {
const resetDate = budgetResetAt ? dayjs(budgetResetAt) : null;
if (resetDate?.isValid()) {
- const cadence = budgetDuration ? `every ${budgetDuration}` : "on a schedule";
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const label = resetDate.format("MMM D, YYYY HH:mm");
+ const period = budgetDuration
+ ? formatBudgetPeriod(getBudgetPeriodStart(budgetDuration, resetDate.utc()).local(), resetDate)
+ : null;
+ const coverage = period
+ ? `Current Period Spend covers ${period} and resets every ${budgetDuration}.`
+ : "Current Period Spend resets on a schedule.";
return {
label,
resetAt: resetDate.toISOString(),
- tooltip: `Current Cycle Spend resets ${cadence}. Next reset ${label} (${timeZone}). ${ROLLOVER_NOTE}`,
+ period,
+ tooltip: `${coverage} Next reset ${label} (${timeZone}). ${ROLLOVER_NOTE}`,
};
}
if (!budgetDuration) {
return {
label: "Never resets",
resetAt: null,
- tooltip:
- "This budget has no reset window, so Current Cycle Spend accumulates for as long as the membership exists.",
+ period: null,
+ tooltip: "This budget has no reset window, so this is lifetime spend for as long as the membership exists.",
};
}
return {
label: "Unknown",
resetAt: null,
+ period: null,
tooltip: `Resets every ${budgetDuration}, but the next reset time has not been scheduled yet. ${ROLLOVER_NOTE}`,
};
}