Skip to content

Commit 08cc1e6

Browse files
authored
Merge pull request BerriAI#26207 from BerriAI/litellm_team_member_total_spend_frontend
Surface per-member budget cycle in Teams > Members tab
2 parents 87e120d + 6b6b8c7 commit 08cc1e6

5 files changed

Lines changed: 63 additions & 7 deletions

File tree

litellm/proxy/_types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3907,7 +3907,7 @@ class OrganizationMemberUpdateResponse(MemberUpdateResponse):
39073907

39083908

39093909
class TeamInfoResponseObjectTeamTable(LiteLLM_TeamTable):
3910-
team_member_budget_table: Optional[LiteLLM_BudgetTable] = None
3910+
team_member_budget_table: Optional[LiteLLM_BudgetTableFull] = None
39113911
# Resources inherited from access groups (separate from direct assignments)
39123912
access_group_models: Optional[List[str]] = None
39133913
access_group_mcp_server_ids: Optional[List[str]] = None

litellm/proxy/management_helpers/utils.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import litellm
1010
from litellm._logging import verbose_logger
1111
from litellm._uuid import uuid
12+
from litellm.proxy.common_utils.timezone_utils import get_budget_reset_time
1213
from litellm.proxy._types import ( # key request types; user request types; team request types; customer request types
1314
BudgetNewRequest,
1415
DeleteCustomerRequest,
@@ -192,6 +193,13 @@ async def _clone_team_default_budget_for_member(
192193
continue
193194
cloned_data[field] = value
194195

196+
# Start the member's budget window at clone time, not the pool's reset
197+
# timestamp — otherwise a member joining mid-cycle inherits a stale reset.
198+
if cloned_data.get("budget_duration"):
199+
cloned_data["budget_reset_at"] = get_budget_reset_time(
200+
cloned_data["budget_duration"]
201+
)
202+
195203
new_budget = await prisma_client.db.litellm_budgettable.create(data=cloned_data)
196204
return new_budget.budget_id
197205

ui/litellm-dashboard/src/components/team/TeamInfo.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export interface TeamMembership {
6060
team_id: string;
6161
budget_id: string;
6262
spend: number;
63+
total_spend: number | null;
6364
litellm_budget_table: {
6465
budget_id: string;
6566
soft_budget: number | null;
@@ -69,6 +70,7 @@ export interface TeamMembership {
6970
rpm_limit: number | null;
7071
model_max_budget: Record<string, number> | null;
7172
budget_duration: string | null;
73+
budget_reset_at: string | null;
7274
allowed_models?: string[] | null;
7375
};
7476
}

ui/litellm-dashboard/src/components/team/TeamMemberTab.tsx

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings";
22
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
33
import { Member } from "@/components/networking";
4+
import { formatBudgetReset } from "@/utils/budgetUtils";
45
import { formatNumberWithCommas } from "@/utils/dataUtils";
56
import { isProxyAdminRole, isUserTeamAdminForSingleTeam } from "@/utils/roles";
67
import { InfoCircleOutlined } from "@ant-design/icons";
@@ -45,11 +46,16 @@ export default function TeamMemberTab({
4546
return "0";
4647
};
4748

48-
// Helper function to get spend for a user
49-
const getUserSpend = (userId: string | null): number | null => {
49+
const getUserCurrentCycleSpend = (userId: string | null): number => {
5050
if (!userId) return 0;
5151
const membership = teamData.team_memberships.find((tm) => tm.user_id === userId);
52-
return membership?.spend || 0;
52+
return membership?.spend ?? 0;
53+
};
54+
55+
const getUserTotalSpend = (userId: string | null): number => {
56+
if (!userId) return 0;
57+
const membership = teamData.team_memberships.find((tm) => tm.user_id === userId);
58+
return membership?.total_spend ?? 0;
5359
};
5460

5561
const getUserBudget = (userId: string | null): string | null => {
@@ -89,6 +95,12 @@ export default function TeamMemberTab({
8995
return models && models.length > 0 ? models : null;
9096
};
9197

98+
const getUserBudgetReset = (userId: string | null): string | null => {
99+
if (!userId) return null;
100+
const membership = teamData.team_memberships.find((tm) => tm.user_id === userId);
101+
return formatBudgetReset(membership?.litellm_budget_table?.budget_reset_at);
102+
};
103+
92104
const extraColumns: ColumnsType<Member> = [
93105
{
94106
title: (
@@ -124,15 +136,29 @@ export default function TeamMemberTab({
124136
{
125137
title: (
126138
<Space direction="horizontal">
127-
Team Member Spend (USD)
128-
<Tooltip title="This is the amount spent by a user in the team.">
139+
Current Cycle Spend (USD)
140+
<Tooltip title="Spend for the current budget cycle. Resets to $0 when the member's budget window rolls over. This is the value checked against the member's budget.">
129141
<InfoCircleOutlined />
130142
</Tooltip>
131143
</Space>
132144
),
133145
key: "spend",
134146
render: (_: unknown, record: Member) => (
135-
<Typography.Text>${formatNumberWithCommas(getUserSpend(record.user_id), 4)}</Typography.Text>
147+
<Typography.Text>${formatNumberWithCommas(getUserCurrentCycleSpend(record.user_id), 4)}</Typography.Text>
148+
),
149+
},
150+
{
151+
title: (
152+
<Space direction="horizontal">
153+
Total Spend (USD)
154+
<Tooltip title="Cumulative spend by this member within this team, across all budget cycles. Tracking began 2026-04-21; spend from before that date is not included.">
155+
<InfoCircleOutlined />
156+
</Tooltip>
157+
</Space>
158+
),
159+
key: "total_spend",
160+
render: (_: unknown, record: Member) => (
161+
<Typography.Text>${formatNumberWithCommas(getUserTotalSpend(record.user_id), 4)}</Typography.Text>
136162
),
137163
},
138164
{
@@ -147,6 +173,18 @@ export default function TeamMemberTab({
147173
);
148174
},
149175
},
176+
{
177+
title: "Budget Reset",
178+
key: "budget_reset",
179+
render: (_: unknown, record: Member) => {
180+
const reset = getUserBudgetReset(record.user_id);
181+
return reset ? (
182+
<Typography.Text>{reset}</Typography.Text>
183+
) : (
184+
<Typography.Text type="secondary"></Typography.Text>
185+
);
186+
},
187+
},
150188
{
151189
title: (
152190
<Space direction="horizontal">
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import dayjs from "dayjs";
2+
3+
export function formatBudgetReset(iso: string | null | undefined): string | null {
4+
if (!iso) return null;
5+
const resetDate = dayjs(iso);
6+
if (!resetDate.isValid()) return null;
7+
return resetDate.format("MMM D, YYYY");
8+
}

0 commit comments

Comments
 (0)