mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
fix: team usage underreporting by fetching all data via aggregated endpoint
This commit is contained in:
parent
f4b79fa635
commit
9cb257ddde
5 changed files with 191 additions and 12 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue