Merge pull request #26207 from BerriAI/litellm_team_member_total_spend_frontend

Surface per-member budget cycle in Teams > Members tab
This commit is contained in:
yuneng-jiang 2026-04-23 17:24:03 -07:00 committed by GitHub
commit 08cc1e66cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 63 additions and 7 deletions

View File

@ -3907,7 +3907,7 @@ class OrganizationMemberUpdateResponse(MemberUpdateResponse):
class TeamInfoResponseObjectTeamTable(LiteLLM_TeamTable):
team_member_budget_table: Optional[LiteLLM_BudgetTable] = None
team_member_budget_table: Optional[LiteLLM_BudgetTableFull] = None
# Resources inherited from access groups (separate from direct assignments)
access_group_models: Optional[List[str]] = None
access_group_mcp_server_ids: Optional[List[str]] = None

View File

@ -9,6 +9,7 @@ from fastapi import HTTPException, Request
import litellm
from litellm._logging import verbose_logger
from litellm._uuid import uuid
from litellm.proxy.common_utils.timezone_utils import get_budget_reset_time
from litellm.proxy._types import ( # key request types; user request types; team request types; customer request types
BudgetNewRequest,
DeleteCustomerRequest,
@ -192,6 +193,13 @@ async def _clone_team_default_budget_for_member(
continue
cloned_data[field] = value
# Start the member's budget window at clone time, not the pool's reset
# timestamp — otherwise a member joining mid-cycle inherits a stale reset.
if cloned_data.get("budget_duration"):
cloned_data["budget_reset_at"] = get_budget_reset_time(
cloned_data["budget_duration"]
)
new_budget = await prisma_client.db.litellm_budgettable.create(data=cloned_data)
return new_budget.budget_id

View File

@ -60,6 +60,7 @@ export interface TeamMembership {
team_id: string;
budget_id: string;
spend: number;
total_spend: number | null;
litellm_budget_table: {
budget_id: string;
soft_budget: number | null;
@ -69,6 +70,7 @@ export interface TeamMembership {
rpm_limit: number | null;
model_max_budget: Record<string, number> | null;
budget_duration: string | null;
budget_reset_at: string | null;
allowed_models?: string[] | null;
};
}

View File

@ -1,6 +1,7 @@
import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { Member } from "@/components/networking";
import { formatBudgetReset } from "@/utils/budgetUtils";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { isProxyAdminRole, isUserTeamAdminForSingleTeam } from "@/utils/roles";
import { InfoCircleOutlined } from "@ant-design/icons";
@ -45,11 +46,16 @@ export default function TeamMemberTab({
return "0";
};
// Helper function to get spend for a user
const getUserSpend = (userId: string | null): number | null => {
const getUserCurrentCycleSpend = (userId: string | null): number => {
if (!userId) return 0;
const membership = teamData.team_memberships.find((tm) => tm.user_id === userId);
return membership?.spend || 0;
return membership?.spend ?? 0;
};
const getUserTotalSpend = (userId: string | null): number => {
if (!userId) return 0;
const membership = teamData.team_memberships.find((tm) => tm.user_id === userId);
return membership?.total_spend ?? 0;
};
const getUserBudget = (userId: string | null): string | null => {
@ -89,6 +95,12 @@ export default function TeamMemberTab({
return models && models.length > 0 ? models : null;
};
const getUserBudgetReset = (userId: string | null): string | null => {
if (!userId) return null;
const membership = teamData.team_memberships.find((tm) => tm.user_id === userId);
return formatBudgetReset(membership?.litellm_budget_table?.budget_reset_at);
};
const extraColumns: ColumnsType<Member> = [
{
title: (
@ -124,15 +136,29 @@ export default function TeamMemberTab({
{
title: (
<Space direction="horizontal">
Team Member Spend (USD)
<Tooltip title="This is the amount spent by a user in the team.">
Current Cycle Spend (USD)
<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.">
<InfoCircleOutlined />
</Tooltip>
</Space>
),
key: "spend",
render: (_: unknown, record: Member) => (
<Typography.Text>${formatNumberWithCommas(getUserSpend(record.user_id), 4)}</Typography.Text>
<Typography.Text>${formatNumberWithCommas(getUserCurrentCycleSpend(record.user_id), 4)}</Typography.Text>
),
},
{
title: (
<Space direction="horizontal">
Total Spend (USD)
<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.">
<InfoCircleOutlined />
</Tooltip>
</Space>
),
key: "total_spend",
render: (_: unknown, record: Member) => (
<Typography.Text>${formatNumberWithCommas(getUserTotalSpend(record.user_id), 4)}</Typography.Text>
),
},
{
@ -147,6 +173,18 @@ export default function TeamMemberTab({
);
},
},
{
title: "Budget Reset",
key: "budget_reset",
render: (_: unknown, record: Member) => {
const reset = getUserBudgetReset(record.user_id);
return reset ? (
<Typography.Text>{reset}</Typography.Text>
) : (
<Typography.Text type="secondary"></Typography.Text>
);
},
},
{
title: (
<Space direction="horizontal">

View File

@ -0,0 +1,8 @@
import dayjs from "dayjs";
export function formatBudgetReset(iso: string | null | undefined): string | null {
if (!iso) return null;
const resetDate = dayjs(iso);
if (!resetDate.isValid()) return null;
return resetDate.format("MMM D, YYYY");
}