diff --git a/litellm/proxy/management_endpoints/tag_management_endpoints.py b/litellm/proxy/management_endpoints/tag_management_endpoints.py index 551c161656..79a69a16c1 100644 --- a/litellm/proxy/management_endpoints/tag_management_endpoints.py +++ b/litellm/proxy/management_endpoints/tag_management_endpoints.py @@ -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, diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py index c8200f7fed..706f7d2c2f 100644 --- a/litellm/proxy/management_endpoints/team_endpoints.py +++ b/litellm/proxy/management_endpoints/team_endpoints.py @@ -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, + ) diff --git a/ui/litellm-dashboard/src/components/entity_usage.tsx b/ui/litellm-dashboard/src/components/entity_usage.tsx index ad9557616e..e4cfb9bfae 100644 --- a/ui/litellm-dashboard/src/components/entity_usage.tsx +++ b/ui/litellm-dashboard/src/components/entity_usage.tsx @@ -3,12 +3,13 @@ import { BarChart, Card, Title, Text, Grid, Col, DateRangePicker, DateRangePickerValue, Table, TableHead, TableRow, TableHeaderCell, TableBody, TableCell, - DonutChart + DonutChart, + TabPanel, TabGroup, TabList, Tab, TabPanels } from "@tremor/react"; 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: { @@ -69,6 +70,8 @@ const EntityUsage: React.FC = ({ } }); + const modelMetrics = processActivityData(spendData); + const [selectedTags, setSelectedTags] = useState([]); const [dateValue, setDateValue] = useState({ from: new Date(Date.now() - 28 * 24 * 60 * 60 * 1000), @@ -89,6 +92,15 @@ const EntityUsage: React.FC = ({ 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"); } @@ -236,240 +248,254 @@ const EntityUsage: React.FC = ({ return filterDataByTags(result); }; + + return (
- - Select Time Range - - - - Filter by {entityType === 'tag' ? 'Tags' : 'Teams'} - + + + + + Cost + Activity + + + + + {/* Total Spend Card */} + + + {entityType === 'tag' ? 'Tag' : 'Team'} Spend Overview + + + Total Spend + + ${spendData.metadata.total_spend.toFixed(2)} + + + + Total Requests + + {spendData.metadata.total_api_requests.toLocaleString()} + + + + Successful Requests + + {spendData.metadata.total_successful_requests.toLocaleString()} + + + + Failed Requests + + {spendData.metadata.total_failed_requests.toLocaleString()} + + + + Total Tokens + + {spendData.metadata.total_tokens.toLocaleString()} + + + + + - {/* Entity Breakdown Section */} - - -
-
- Spend Per {entityType === 'tag' ? 'Tag' : 'Team'} -
- Get Started Tracking cost per {entityType} - - here - -
-
- - + {/* Daily Spend Chart */} + + + Daily Spend + new Date(a.date).getTime() - new Date(b.date).getTime() + )} + index="date" + categories={["metrics.spend"]} + colors={["cyan"]} + valueFormatter={(value) => `$${value.toFixed(2)}`} + yAxisWidth={100} + showLegend={false} + /> + + + + {/* Entity Breakdown Section */} + + +
+
+ Spend Per {entityType === 'tag' ? 'Tag' : 'Team'} +
+ Get Started Tracking cost per {entityType} + + here + +
+
+ + + `$${value.toFixed(4)}`} + layout="vertical" + showLegend={false} + yAxisWidth={100} + /> + + + + + + {entityType === 'tag' ? 'Tag' : 'Team'} + Spend + Successful + Failed + Tokens + + + + {getEntityBreakdown() + .filter(entity => entity.spend > 0) + .map((entity) => ( + + {entity.entity} + ${entity.spend.toFixed(4)} + + {entity.successful_requests.toLocaleString()} + + + {entity.failed_requests.toLocaleString()} + + {entity.tokens.toLocaleString()} + + ))} + +
+ +
+
+
+ + + + {/* Top API Keys */} + + + Top API Keys + `$${value.toFixed(4)}`} - layout="vertical" - showLegend={false} - yAxisWidth={100} - /> - - - - - - {entityType === 'tag' ? 'Tag' : 'Team'} - Spend - Successful - Failed - Tokens - - - - {getEntityBreakdown() - .filter(entity => entity.spend > 0) - .map((entity) => ( - - {entity.entity} - ${entity.spend.toFixed(4)} - - {entity.successful_requests.toLocaleString()} - - - {entity.failed_requests.toLocaleString()} - - {entity.tokens.toLocaleString()} - - ))} - -
- -
-
-
- - - - {/* Top API Keys */} - - - Top API Keys - `$${value.toFixed(2)}`} - layout="vertical" - yAxisWidth={200} - showLegend={false} - /> - - - - {/* Top Models */} - - - Top Models - `$${value.toFixed(2)}`} - layout="vertical" - yAxisWidth={200} - showLegend={false} - /> - - - - - - {/* Spend by Provider */} - - -
- Provider Usage - - - `$${value.toFixed(2)}`} - colors={["cyan", "blue", "indigo", "violet", "purple"]} + layout="vertical" + yAxisWidth={200} + showLegend={false} /> - - - - - - Provider - Spend - Successful - Failed - Tokens - - - - {getProviderSpend().map((provider) => ( - - {provider.provider} - ${provider.spend.toFixed(2)} - - {provider.successful_requests.toLocaleString()} - - - {provider.failed_requests.toLocaleString()} - - {provider.tokens.toLocaleString()} - - ))} - -
- -
-
-
- -
+ + + + {/* Top Models */} + + + Top Models + `$${value.toFixed(2)}`} + layout="vertical" + yAxisWidth={200} + showLegend={false} + /> + + + + + + {/* Spend by Provider */} + + +
+ Provider Usage + + + `$${value.toFixed(2)}`} + colors={["cyan", "blue", "indigo", "violet", "purple"]} + /> + + + + + + Provider + Spend + Successful + Failed + Tokens + + + + {getProviderSpend().map((provider) => ( + + {provider.provider} + ${provider.spend.toFixed(2)} + + {provider.successful_requests.toLocaleString()} + + + {provider.failed_requests.toLocaleString()} + + {provider.tokens.toLocaleString()} + + ))} + +
+ +
+
+
+ + +
+ + + +
+
); }; diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 288f2ed041..70aa218508 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -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 diff --git a/ui/litellm-dashboard/src/components/new_usage.tsx b/ui/litellm-dashboard/src/components/new_usage.tsx index 6c1ee1405e..aab1ae3d0d 100644 --- a/ui/litellm-dashboard/src/components/new_usage.tsx +++ b/ui/litellm-dashboard/src/components/new_usage.tsx @@ -236,6 +236,7 @@ const NewUsagePage: React.FC = ({ Your Usage Tag Usage + Team Usage {/* Your Usage Panel */} @@ -469,6 +470,14 @@ const NewUsagePage: React.FC = ({ entityType="tag" /> + + {/* Team Usage Panel */} + + +