fix: team usage underreporting by fetching all data via aggregated endpoint

This commit is contained in:
naaa760 2026-02-17 19:06:29 +05:30
parent f4b79fa635
commit 9cb257ddde
5 changed files with 191 additions and 12 deletions

View file

@ -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

View file

@ -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:

View file

@ -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,
)

View file

@ -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<EntityUsageProps> = ({ 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,

View file

@ -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,