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:
commit
08cc1e66cf
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@ -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">
|
||||
|
||||
8
ui/litellm-dashboard/src/utils/budgetUtils.ts
Normal file
8
ui/litellm-dashboard/src/utils/budgetUtils.ts
Normal 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");
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user