feat(ui/): add team based usage to dashboard

allows admin to see spend across teams + within teams at 1m+ spend logs
This commit is contained in:
Krrish Dholakia 2025-04-16 15:38:39 -07:00
parent c0d7e9f16d
commit 0eecfb7d5c
5 changed files with 131 additions and 16 deletions

View file

@ -364,6 +364,7 @@ async def delete_tag(
"/tag/daily/activity",
response_model=SpendAnalyticsPaginatedResponse,
tags=["tag management"],
dependencies=[Depends(user_api_key_auth)],
)
async def get_tag_daily_activity(
tags: Optional[str] = None,

View file

@ -62,6 +62,9 @@ from litellm.proxy.management_endpoints.common_utils import (
_is_user_team_admin,
_set_object_metadata_field,
)
from litellm.proxy.management_endpoints.tag_management_endpoints import (
get_daily_activity,
)
from litellm.proxy.management_helpers.team_member_permission_checks import (
TeamMemberPermissionChecks,
)
@ -75,6 +78,9 @@ from litellm.proxy.utils import (
handle_exception_on_proxy,
)
from litellm.router import Router
from litellm.types.proxy.management_endpoints.common_daily_activity import (
SpendAnalyticsPaginatedResponse,
)
from litellm.types.proxy.management_endpoints.team_endpoints import (
GetTeamMemberPermissionsResponse,
UpdateTeamMemberPermissionsRequest,
@ -515,12 +521,12 @@ async def update_team(
updated_kv["model_id"] = _model_id
updated_kv = prisma_client.jsonify_team_object(db_data=updated_kv)
team_row: Optional[LiteLLM_TeamTable] = (
await prisma_client.db.litellm_teamtable.update(
where={"team_id": data.team_id},
data=updated_kv,
include={"litellm_model_table": True}, # type: ignore
)
team_row: Optional[
LiteLLM_TeamTable
] = await prisma_client.db.litellm_teamtable.update(
where={"team_id": data.team_id},
data=updated_kv,
include={"litellm_model_table": True}, # type: ignore
)
if team_row is None or team_row.team_id is None:
@ -1146,10 +1152,10 @@ async def delete_team(
team_rows: List[LiteLLM_TeamTable] = []
for team_id in data.team_ids:
try:
team_row_base: Optional[BaseModel] = (
await prisma_client.db.litellm_teamtable.find_unique(
where={"team_id": team_id}
)
team_row_base: Optional[
BaseModel
] = await prisma_client.db.litellm_teamtable.find_unique(
where={"team_id": team_id}
)
if team_row_base is None:
raise Exception
@ -1307,10 +1313,10 @@ async def team_info(
)
try:
team_info: Optional[BaseModel] = (
await prisma_client.db.litellm_teamtable.find_unique(
where={"team_id": team_id}
)
team_info: Optional[
BaseModel
] = await prisma_client.db.litellm_teamtable.find_unique(
where={"team_id": team_id}
)
if team_info is None:
raise Exception
@ -2079,3 +2085,52 @@ async def update_team_member_permissions(
)
return updated_team
@router.get(
"/team/daily/activity",
response_model=SpendAnalyticsPaginatedResponse,
tags=["team management"],
dependencies=[Depends(user_api_key_auth)],
)
async def get_team_daily_activity(
team_ids: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
model: Optional[str] = None,
api_key: Optional[str] = None,
page: int = 1,
page_size: int = 10,
):
"""
Get daily activity for specific teams or all teams.
Args:
team_ids (Optional[str]): Comma-separated list of team IDs to filter by. If not provided, returns data for all teams.
start_date (Optional[str]): Start date for the activity period (YYYY-MM-DD).
end_date (Optional[str]): End date for the activity period (YYYY-MM-DD).
model (Optional[str]): Filter by model name.
api_key (Optional[str]): Filter by API key.
page (int): Page number for pagination.
page_size (int): Number of items per page.
Returns:
SpendAnalyticsPaginatedResponse: Paginated response containing daily activity data.
"""
from litellm.proxy.proxy_server import prisma_client
# Convert comma-separated tags string to list if provided
team_ids_list = team_ids.split(",") if team_ids else None
return await get_daily_activity(
prisma_client=prisma_client,
table_name="litellm_dailyteamspend",
entity_id_field="team_id",
entity_id=team_ids_list,
start_date=start_date,
end_date=end_date,
model=model,
api_key=api_key,
page=page,
page_size=page_size,
)

View file

@ -8,7 +8,7 @@ import {
import { Select } from 'antd';
import { ActivityMetrics, processActivityData } from './activity_metrics';
import { SpendMetrics, DailyData } from './usage/types';
import { tagDailyActivityCall } from './networking';
import { tagDailyActivityCall, teamDailyActivityCall } from './networking';
interface EntityMetrics {
metrics: {
@ -89,6 +89,15 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
selectedTags.length > 0 ? selectedTags : null
);
setSpendData(data);
} else if (entityType === 'team') {
const data = await teamDailyActivityCall(
accessToken,
startTime,
endTime,
1,
selectedTags.length > 0 ? selectedTags : null
);
setSpendData(data);
} else {
throw new Error("Invalid entity type");
}
@ -400,7 +409,7 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
/>
</Card>
</Col>
{/* Top Models */}
<Col numColSpan={1}>
<Card>

View file

@ -1186,6 +1186,47 @@ export const tagDailyActivityCall = async (accessToken: String, startTime: Date,
}
};
export const teamDailyActivityCall = async (accessToken: String, startTime: Date, endTime: Date, page: number = 1, teamIds: string[] | null = null) => {
/**
* Get daily user activity on proxy
*/
try {
let url = proxyBaseUrl ? `${proxyBaseUrl}/team/daily/activity` : `/team/daily/activity`;
const queryParams = new URLSearchParams();
queryParams.append('start_date', startTime.toISOString());
queryParams.append('end_date', endTime.toISOString());
queryParams.append('page_size', '1000');
queryParams.append('page', page.toString());
if (teamIds) {
queryParams.append('team_ids', teamIds.join(','));
}
const queryString = queryParams.toString();
if (queryString) {
url += `?${queryString}`;
}
const response = await fetch(url, {
method: "GET",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
const errorData = await response.text();
handleError(errorData);
throw new Error("Network response was not ok");
}
const data = await response.json();
return data;
} catch (error) {
console.error("Failed to create key:", error);
throw error;
}
};
export const getTotalSpendCall = async (accessToken: String) => {
/**
* Get all models on proxy

View file

@ -236,6 +236,7 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
<TabList variant="solid" className="mt-1">
<Tab>Your Usage</Tab>
<Tab>Tag Usage</Tab>
<Tab>Team Usage</Tab>
</TabList>
<TabPanels>
{/* Your Usage Panel */}
@ -469,6 +470,14 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
entityType="tag"
/>
</TabPanel>
{/* Team Usage Panel */}
<TabPanel>
<EntityUsage
accessToken={accessToken}
entityType="team"
/>
</TabPanel>
</TabPanels>
</TabGroup>
</div>