From 9cb257dddef80e46429610f7e337d2a9d02af125 Mon Sep 17 00:00:00 2001 From: naaa760 Date: Tue, 17 Feb 2026 19:06:29 +0530 Subject: [PATCH] fix: team usage underreporting by fetching all data via aggregated endpoint --- litellm/proxy/_types.py | 3 + .../common_daily_activity.py | 2 +- .../management_endpoints/team_endpoints.py | 121 ++++++++++++++++++ .../components/EntityUsage/EntityUsage.tsx | 28 ++-- .../src/components/networking.tsx | 49 ++++++- 5 files changed, 191 insertions(+), 12 deletions(-) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index c327dd130a5..e62500df0c2 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -529,6 +529,7 @@ class LiteLLMRoutes(enum.Enum): "/team/permissions_list", "/team/permissions_update", "/team/daily/activity", + "/team/daily/activity/aggregated", # model "/model/new", "/model/update", @@ -629,6 +630,7 @@ class LiteLLMRoutes(enum.Enum): "/team/permissions_list", "/team/permissions_update", "/team/daily/activity", + "/team/daily/activity/aggregated", "/model/new", "/model/update", "/model/delete", @@ -658,6 +660,7 @@ class LiteLLMRoutes(enum.Enum): "/user/available_roles", "/user/daily/activity", "/team/daily/activity", + "/team/daily/activity/aggregated", "/tag/daily/activity", "/tag/list", ] + info_routes diff --git a/litellm/proxy/management_endpoints/common_daily_activity.py b/litellm/proxy/management_endpoints/common_daily_activity.py index e5df2f82f69..91062045887 100644 --- a/litellm/proxy/management_endpoints/common_daily_activity.py +++ b/litellm/proxy/management_endpoints/common_daily_activity.py @@ -619,7 +619,7 @@ async def get_daily_activity_aggregated( start_date: Optional[str], end_date: Optional[str], model: Optional[str], - api_key: Optional[str], + api_key: Optional[Union[str, List[str]]], exclude_entity_ids: Optional[List[str]] = None, timezone_offset_minutes: Optional[int] = None, ) -> SpendAnalyticsPaginatedResponse: diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py index f5aabdfcedb..2d8aae03012 100644 --- a/litellm/proxy/management_endpoints/team_endpoints.py +++ b/litellm/proxy/management_endpoints/team_endpoints.py @@ -76,6 +76,9 @@ from litellm.proxy.management_endpoints.common_utils import ( _upsert_budget_and_membership, _user_has_admin_view, ) +from litellm.proxy.management_endpoints.common_daily_activity import ( + get_daily_activity_aggregated, +) from litellm.proxy.management_endpoints.tag_management_endpoints import ( get_daily_activity, ) @@ -4015,3 +4018,121 @@ async def get_team_daily_activity( page=page, page_size=page_size, ) + + +@router.get( + "/team/daily/activity/aggregated", + response_model=SpendAnalyticsPaginatedResponse, + tags=["team management"], +) +async def get_team_daily_activity_aggregated( + team_ids: Optional[str] = None, + start_date: Optional[str] = None, + end_date: Optional[str] = None, + model: Optional[str] = None, + api_key: Optional[str] = None, + exclude_team_ids: Optional[str] = None, + timezone: Optional[int] = None, + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): + """ + Get full daily activity for teams without pagination. + Returns all data from LiteLLM_DailyTeamSpend for accurate totals. + """ + from litellm.proxy.proxy_server import ( + prisma_client, + proxy_logging_obj, + user_api_key_cache, + ) + + if prisma_client is None: + raise HTTPException( + status_code=500, + detail={"error": CommonProxyErrors.db_not_connected_error.value}, + ) + + team_ids_list = team_ids.split(",") if team_ids else None + exclude_team_ids_list: Optional[List[str]] = None + if exclude_team_ids: + exclude_team_ids_list = ( + exclude_team_ids.split(",") if exclude_team_ids else None + ) + + if not _user_has_admin_view(user_api_key_dict): + user_info = await get_user_object( + user_id=user_api_key_dict.user_id, + prisma_client=prisma_client, + user_id_upsert=False, + user_api_key_cache=user_api_key_cache, + parent_otel_span=user_api_key_dict.parent_otel_span, + proxy_logging_obj=proxy_logging_obj, + check_db_only=True, + ) + if user_info is None: + raise HTTPException( + status_code=404, + detail={ + "error": "User= {} not found".format(user_api_key_dict.user_id) + }, + ) + + if team_ids_list is None: + team_ids_list = user_info.teams + else: + for team_id in team_ids_list: + if team_id not in user_info.teams: + raise HTTPException( + status_code=404, + detail={ + "error": "User does not belong to Team= {}. Call `/user/info` to see user's teams".format( + team_id + ) + }, + ) + + where_condition = {} + if team_ids_list: + where_condition["team_id"] = {"in": list(team_ids_list)} + team_aliases = await prisma_client.db.litellm_teamtable.find_many( + where=where_condition + ) + team_alias_metadata = { + t.team_id: {"team_alias": t.team_alias} for t in team_aliases + } + + user_api_keys: Optional[List[str]] = None + if not _user_has_admin_view(user_api_key_dict) and team_ids_list and team_aliases: + is_team_admin_for_any = False + for team_alias in team_aliases: + team_obj = LiteLLM_TeamTable(**team_alias.model_dump()) + if _is_user_team_admin( + user_api_key_dict=user_api_key_dict, team_obj=team_obj + ): + is_team_admin_for_any = True + break + + if not is_team_admin_for_any: + user_keys = await prisma_client.db.litellm_verificationtoken.find_many( + where={"user_id": user_api_key_dict.user_id} + ) + user_api_keys = [key.token for key in user_keys if key.token] + if not user_api_keys: + user_api_keys = [""] + + final_api_key_filter: Optional[Union[str, List[str]]] = api_key + if final_api_key_filter is None and user_api_keys is not None: + final_api_key_filter = user_api_keys + + return await get_daily_activity_aggregated( + prisma_client=prisma_client, + table_name="litellm_dailyteamspend", + entity_id_field="team_id", + entity_id=team_ids_list, + entity_metadata_field=team_alias_metadata, + exclude_entity_ids=exclude_team_ids_list, + start_date=start_date, + end_date=end_date, + model=model, + api_key=final_api_key_filter, + timezone_offset_minutes=timezone, + ) diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.tsx index 32882341921..899229bc600 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.tsx +++ b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.tsx @@ -31,6 +31,7 @@ import { customerDailyActivityCall, organizationDailyActivityCall, tagDailyActivityCall, + teamDailyActivityAggregatedCall, teamDailyActivityCall, } from "../../../networking"; import { getProviderLogoAndName } from "../../../provider_info_helpers"; @@ -121,14 +122,25 @@ const EntityUsage: React.FC = ({ accessToken, entityType, enti ); setSpendData(data); } else if (entityType === "team") { - const data = await teamDailyActivityCall( - accessToken, - startTime, - endTime, - 1, - selectedTags.length > 0 ? selectedTags : null, - ); - setSpendData(data); + const teamIds = selectedTags.length > 0 ? selectedTags : null; + try { + const data = await teamDailyActivityAggregatedCall( + accessToken, + startTime, + endTime, + teamIds, + ); + setSpendData(data); + } catch { + const data = await teamDailyActivityCall( + accessToken, + startTime, + endTime, + 1, + teamIds, + ); + setSpendData(data); + } } else if (entityType === "organization") { const data = await organizationDailyActivityCall( accessToken, diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 35e713a94c1..de7e28bd21f 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -1771,9 +1771,6 @@ export const teamDailyActivityCall = async ( page: number = 1, teamIds: string[] | null = null, ) => { - /** - * Get daily user activity on proxy - */ return fetchDailyActivity({ accessToken, endpoint: "/team/daily/activity", @@ -1787,6 +1784,52 @@ export const teamDailyActivityCall = async ( }); }; +export const teamDailyActivityAggregatedCall = async ( + accessToken: string, + startTime: Date, + endTime: Date, + teamIds: string[] | null = null, +) => { + try { + const formatDate = (date: Date) => { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; + }; + const baseUrl = proxyBaseUrl ? `${proxyBaseUrl}/team/daily/activity/aggregated` : `/team/daily/activity/aggregated`; + const params = new URLSearchParams(); + params.append("start_date", formatDate(startTime)); + params.append("end_date", formatDate(endTime)); + params.append("timezone", new Date().getTimezoneOffset().toString()); + params.append("exclude_team_ids", "litellm-dashboard"); + if (teamIds && teamIds.length > 0) { + params.append("team_ids", teamIds.join(",")); + } + const url = `${baseUrl}?${params.toString()}`; + + const response = await fetch(url, { + method: "GET", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + handleError(errorMessage); + throw new Error(errorMessage); + } + + return await response.json(); + } catch (error) { + console.error("Failed to fetch aggregated team daily activity:", error); + throw error; + } +}; + export const organizationDailyActivityCall = async ( accessToken: string, startTime: Date,