Show usage by key (on all up, team, and tag usage dashboards) (#10152)

* fix(entity_usage.tsx): allow user to select team in team usage tab

* fix(new_usage.tsx): load all tags for filtering

* fix(tag_management_endpoints.py): return dynamic tags from db on `/tag/list`

* fix(litellm_pre_call_utils.py): support x-litellm-tags even if tag based routing not enabled

* fix(new_usage.tsx): show breakdown of usage by api key on dashboard

helpful when looking at spend by team

* fix(networking.tsx): exclude litellm-dashboard team id's from calls

adds noisy ui tokens to key activity

* fix(new_usage.tsx): allow user to see activity by key on main tab

* feat(internal_user_endpoints.py): refactor to use common_daily_activity function

reuses same logic across teams/keys/tags

Allows returning team_alias in api_keys consistently

* fix(leftnav.tsx): swap old usage with new usage tab

* fix(entity_usage.tsx): show breakdown of teams in daily spend chart

* style(new_usage.tsx): show global usage tab if user is admin / has admin view

* fix(new_usage.tsx): add disclaimer for new usage dashboard

* fix(new_usage.tsx): fix linting error
This commit is contained in:
Krish Dholakia 2025-04-18 21:53:54 -07:00 committed by GitHub
parent b6ffa5425a
commit 2c1d1f7193
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 221 additions and 194 deletions

View file

@ -433,14 +433,13 @@ class LiteLLMProxyRequestSetup:
) -> Optional[List[str]]: ) -> Optional[List[str]]:
tags = None tags = None
if llm_router and llm_router.enable_tag_filtering is True: # Check request headers for tags
# Check request headers for tags if "x-litellm-tags" in headers:
if "x-litellm-tags" in headers: if isinstance(headers["x-litellm-tags"], str):
if isinstance(headers["x-litellm-tags"], str): _tags = headers["x-litellm-tags"].split(",")
_tags = headers["x-litellm-tags"].split(",") tags = [tag.strip() for tag in _tags]
tags = [tag.strip() for tag in _tags] elif isinstance(headers["x-litellm-tags"], list):
elif isinstance(headers["x-litellm-tags"], list): tags = headers["x-litellm-tags"]
tags = headers["x-litellm-tags"]
# Check request body for tags # Check request body for tags
if "tags" in data and isinstance(data["tags"], list): if "tags" in data and isinstance(data["tags"], list):
tags = data["tags"] tags = data["tags"]

View file

@ -75,7 +75,8 @@ def update_breakdown_metrics(
metadata=KeyMetadata( metadata=KeyMetadata(
key_alias=api_key_metadata.get(record.api_key, {}).get( key_alias=api_key_metadata.get(record.api_key, {}).get(
"key_alias", None "key_alias", None
) ),
team_id=api_key_metadata.get(record.api_key, {}).get("team_id", None),
), # Add any api_key-specific metadata here ), # Add any api_key-specific metadata here
) )
breakdown.api_keys[record.api_key].metrics = update_metrics( breakdown.api_keys[record.api_key].metrics = update_metrics(
@ -112,6 +113,7 @@ async def get_daily_activity(
api_key: Optional[str], api_key: Optional[str],
page: int, page: int,
page_size: int, page_size: int,
exclude_entity_ids: Optional[List[str]] = None,
) -> SpendAnalyticsPaginatedResponse: ) -> SpendAnalyticsPaginatedResponse:
"""Common function to get daily activity for any entity type.""" """Common function to get daily activity for any entity type."""
if prisma_client is None: if prisma_client is None:
@ -144,6 +146,10 @@ async def get_daily_activity(
where_conditions[entity_id_field] = {"in": entity_id} where_conditions[entity_id_field] = {"in": entity_id}
else: else:
where_conditions[entity_id_field] = entity_id where_conditions[entity_id_field] = entity_id
if exclude_entity_ids:
where_conditions.setdefault(entity_id_field, {})["not"] = {
"in": exclude_entity_ids
}
# Get total count for pagination # Get total count for pagination
total_count = await getattr(prisma_client.db, table_name).count( total_count = await getattr(prisma_client.db, table_name).count(
@ -175,7 +181,10 @@ async def get_daily_activity(
where={"token": {"in": list(api_keys)}} where={"token": {"in": list(api_keys)}}
) )
api_key_metadata.update( api_key_metadata.update(
{k.token: {"key_alias": k.key_alias} for k in key_records} {
k.token: {"key_alias": k.key_alias, "team_id": k.team_id}
for k in key_records
}
) )
# Process results # Process results

View file

@ -25,6 +25,8 @@ from litellm._logging import verbose_proxy_logger
from litellm.litellm_core_utils.duration_parser import duration_in_seconds from litellm.litellm_core_utils.duration_parser import duration_in_seconds
from litellm.proxy._types import * from litellm.proxy._types import *
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.management_endpoints.common_daily_activity import get_daily_activity
from litellm.proxy.management_endpoints.common_utils import _user_has_admin_view
from litellm.proxy.management_endpoints.key_management_endpoints import ( from litellm.proxy.management_endpoints.key_management_endpoints import (
generate_key_helper_fn, generate_key_helper_fn,
prepare_metadata_fields, prepare_metadata_fields,
@ -1382,136 +1384,22 @@ async def get_user_daily_activity(
) )
try: try:
# Build filter conditions entity_id: Optional[str] = None
where_conditions: Dict[str, Any] = { if not _user_has_admin_view(user_api_key_dict):
"date": { entity_id = user_api_key_dict.user_id
"gte": start_date,
"lte": end_date,
}
}
if model: return await get_daily_activity(
where_conditions["model"] = model prisma_client=prisma_client,
if api_key: table_name="litellm_dailyuserspend",
where_conditions["api_key"] = api_key entity_id_field="user_id",
entity_id=entity_id,
if ( entity_metadata_field=None,
user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN start_date=start_date,
and user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY end_date=end_date,
): model=model,
where_conditions[ api_key=api_key,
"user_id" page=page,
] = user_api_key_dict.user_id # only allow access to own data page_size=page_size,
# Get total count for pagination
total_count = await prisma_client.db.litellm_dailyuserspend.count(
where=where_conditions
)
# Fetch paginated results
daily_spend_data = await prisma_client.db.litellm_dailyuserspend.find_many(
where=where_conditions,
order=[
{"date": "desc"},
],
skip=(page - 1) * page_size,
take=page_size,
)
daily_spend_data_pydantic_list = [
LiteLLM_DailyUserSpend(**record.model_dump()) for record in daily_spend_data
]
# Get all unique API keys from the spend data
api_keys = set()
for record in daily_spend_data_pydantic_list:
if record.api_key:
api_keys.add(record.api_key)
# Fetch key aliases in bulk
api_key_metadata: Dict[str, Dict[str, Any]] = {}
model_metadata: Dict[str, Dict[str, Any]] = {}
provider_metadata: Dict[str, Dict[str, Any]] = {}
if api_keys:
key_records = await prisma_client.db.litellm_verificationtoken.find_many(
where={"token": {"in": list(api_keys)}}
)
api_key_metadata.update(
{k.token: {"key_alias": k.key_alias} for k in key_records}
)
# Process results
results = []
total_metrics = SpendMetrics()
# Group data by date and other dimensions
grouped_data: Dict[str, Dict[str, Any]] = {}
for record in daily_spend_data_pydantic_list:
date_str = record.date
if date_str not in grouped_data:
grouped_data[date_str] = {
"metrics": SpendMetrics(),
"breakdown": BreakdownMetrics(),
}
# Update metrics
grouped_data[date_str]["metrics"] = update_metrics(
grouped_data[date_str]["metrics"], record
)
# Update breakdowns
grouped_data[date_str]["breakdown"] = update_breakdown_metrics(
grouped_data[date_str]["breakdown"],
record,
model_metadata,
provider_metadata,
api_key_metadata,
)
# Update total metrics
total_metrics.spend += record.spend
total_metrics.prompt_tokens += record.prompt_tokens
total_metrics.completion_tokens += record.completion_tokens
total_metrics.total_tokens += (
record.prompt_tokens + record.completion_tokens
)
total_metrics.cache_read_input_tokens += record.cache_read_input_tokens
total_metrics.cache_creation_input_tokens += (
record.cache_creation_input_tokens
)
total_metrics.api_requests += record.api_requests
total_metrics.successful_requests += record.successful_requests
total_metrics.failed_requests += record.failed_requests
# Convert grouped data to response format
for date_str, data in grouped_data.items():
results.append(
DailySpendData(
date=datetime.strptime(date_str, "%Y-%m-%d").date(),
metrics=data["metrics"],
breakdown=data["breakdown"],
)
)
# Sort results by date
results.sort(key=lambda x: x.date, reverse=True)
return SpendAnalyticsPaginatedResponse(
results=results,
metadata=DailySpendMetadata(
total_spend=total_metrics.spend,
total_prompt_tokens=total_metrics.prompt_tokens,
total_completion_tokens=total_metrics.completion_tokens,
total_tokens=total_metrics.total_tokens,
total_api_requests=total_metrics.api_requests,
total_successful_requests=total_metrics.successful_requests,
total_failed_requests=total_metrics.failed_requests,
total_cache_read_input_tokens=total_metrics.cache_read_input_tokens,
total_cache_creation_input_tokens=total_metrics.cache_creation_input_tokens,
page=page,
total_pages=-(-total_count // page_size), # Ceiling division
has_more=(page * page_size) < total_count,
),
) )
except Exception as e: except Exception as e:

View file

@ -12,7 +12,7 @@ All /tag management endpoints
import datetime import datetime
import json import json
from typing import Dict, Optional from typing import Dict, List, Optional
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
@ -25,6 +25,7 @@ from litellm.proxy.management_endpoints.common_daily_activity import (
get_daily_activity, get_daily_activity,
) )
from litellm.types.tag_management import ( from litellm.types.tag_management import (
LiteLLM_DailyTagSpendTable,
TagConfig, TagConfig,
TagDeleteRequest, TagDeleteRequest,
TagInfoRequest, TagInfoRequest,
@ -301,6 +302,7 @@ async def info_tag(
"/tag/list", "/tag/list",
tags=["tag management"], tags=["tag management"],
dependencies=[Depends(user_api_key_auth)], dependencies=[Depends(user_api_key_auth)],
response_model=List[TagConfig],
) )
async def list_tags( async def list_tags(
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
@ -314,9 +316,33 @@ async def list_tags(
raise HTTPException(status_code=500, detail="Database not connected") raise HTTPException(status_code=500, detail="Database not connected")
try: try:
## QUERY STORED TAGS ##
tags_config = await _get_tags_config(prisma_client) tags_config = await _get_tags_config(prisma_client)
list_of_tags = list(tags_config.values()) list_of_tags = list(tags_config.values())
return list_of_tags
## QUERY DYNAMIC TAGS ##
dynamic_tags = await prisma_client.db.litellm_dailytagspend.find_many(
distinct=["tag"],
)
dynamic_tags_list = [
LiteLLM_DailyTagSpendTable(**dynamic_tag.model_dump())
for dynamic_tag in dynamic_tags
]
dynamic_tag_config = [
TagConfig(
name=tag.tag,
description="This is just a spend tag that was passed dynamically in a request. It does not control any LLM models.",
models=None,
created_at=tag.created_at.isoformat(),
updated_at=tag.updated_at.isoformat(),
)
for tag in dynamic_tags_list
if tag.tag not in tags_config
]
return list_of_tags + dynamic_tag_config
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))

View file

@ -2102,6 +2102,7 @@ async def get_team_daily_activity(
api_key: Optional[str] = None, api_key: Optional[str] = None,
page: int = 1, page: int = 1,
page_size: int = 10, page_size: int = 10,
exclude_team_ids: Optional[str] = None,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
): ):
""" """
@ -2115,7 +2116,7 @@ async def get_team_daily_activity(
api_key (Optional[str]): Filter by API key. api_key (Optional[str]): Filter by API key.
page (int): Page number for pagination. page (int): Page number for pagination.
page_size (int): Number of items per page. page_size (int): Number of items per page.
exclude_team_ids (Optional[str]): Comma-separated list of team IDs to exclude.
Returns: Returns:
SpendAnalyticsPaginatedResponse: Paginated response containing daily activity data. SpendAnalyticsPaginatedResponse: Paginated response containing daily activity data.
""" """
@ -2133,6 +2134,12 @@ async def get_team_daily_activity(
# Convert comma-separated tags string to list if provided # Convert comma-separated tags string to list if provided
team_ids_list = team_ids.split(",") if team_ids else None 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): if not _user_has_admin_view(user_api_key_dict):
user_info = await get_user_object( user_info = await get_user_object(
@ -2182,6 +2189,7 @@ async def get_team_daily_activity(
entity_id_field="team_id", entity_id_field="team_id",
entity_id=team_ids_list, entity_id=team_ids_list,
entity_metadata_field=team_alias_metadata, entity_metadata_field=team_alias_metadata,
exclude_entity_ids=exclude_team_ids_list,
start_date=start_date, start_date=start_date,
end_date=end_date, end_date=end_date,
model=model, model=model,

View file

@ -39,6 +39,7 @@ class KeyMetadata(BaseModel):
"""Metadata for a key""" """Metadata for a key"""
key_alias: Optional[str] = None key_alias: Optional[str] = None
team_id: Optional[str] = None
class KeyMetricWithMetadata(MetricBase): class KeyMetricWithMetadata(MetricBase):

View file

@ -1,3 +1,4 @@
from datetime import datetime
from typing import Dict, List, Optional from typing import Dict, List, Optional
from pydantic import BaseModel from pydantic import BaseModel
@ -30,3 +31,23 @@ class TagDeleteRequest(BaseModel):
class TagInfoRequest(BaseModel): class TagInfoRequest(BaseModel):
names: List[str] names: List[str]
class LiteLLM_DailyTagSpendTable(BaseModel):
id: str
tag: str
date: str
api_key: str
model: str
model_group: Optional[str]
custom_llm_provider: Optional[str]
prompt_tokens: int
completion_tokens: int
cache_read_input_tokens: int
cache_creation_input_tokens: int
spend: float
api_requests: int
successful_requests: int
failed_requests: int
created_at: datetime
updated_at: datetime

View file

@ -370,6 +370,7 @@ export default function CreateKeyPage() {
userID={userID} userID={userID}
userRole={userRole} userRole={userRole}
accessToken={accessToken} accessToken={accessToken}
teams={teams}
/> />
) : ) :
( (

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { Card, Grid, Text, Title, Accordion, AccordionHeader, AccordionBody } from '@tremor/react'; import { Card, Grid, Text, Title, Accordion, AccordionHeader, AccordionBody } from '@tremor/react';
import { AreaChart, BarChart } from '@tremor/react'; import { AreaChart, BarChart } from '@tremor/react';
import { SpendMetrics, DailyData, ModelActivityData } from './usage/types'; import { SpendMetrics, DailyData, ModelActivityData, MetricWithMetadata, KeyMetricWithMetadata } from './usage/types';
import { Collapse } from 'antd'; import { Collapse } from 'antd';
interface ActivityMetricsProps { interface ActivityMetricsProps {
@ -224,7 +224,7 @@ export const ActivityMetrics: React.FC<ActivityMetricsProps> = ({ modelMetrics }
key={modelName} key={modelName}
header={ header={
<div className="flex justify-between items-center w-full"> <div className="flex justify-between items-center w-full">
<Title>{modelName || 'Unknown Model'}</Title> <Title>{modelMetrics[modelName].label || 'Unknown Item'}</Title>
<div className="flex space-x-4 text-sm text-gray-500"> <div className="flex space-x-4 text-sm text-gray-500">
<span>${modelMetrics[modelName].total_spend.toFixed(2)}</span> <span>${modelMetrics[modelName].total_spend.toFixed(2)}</span>
<span>{modelMetrics[modelName].total_requests.toLocaleString()} requests</span> <span>{modelMetrics[modelName].total_requests.toLocaleString()} requests</span>
@ -243,14 +243,24 @@ export const ActivityMetrics: React.FC<ActivityMetricsProps> = ({ modelMetrics }
); );
}; };
// Helper function to format key label
const formatKeyLabel = (modelData: KeyMetricWithMetadata, model: string): string => {
const keyAlias = modelData.metadata.key_alias || `key-hash-${model}`;
const teamId = modelData.metadata.team_id;
return teamId ? `${keyAlias} (team_id: ${teamId})` : keyAlias;
};
// Process data function // Process data function
export const processActivityData = (dailyActivity: { results: DailyData[] }): Record<string, ModelActivityData> => { export const processActivityData = (dailyActivity: { results: DailyData[] }, key: "models" | "api_keys"): Record<string, ModelActivityData> => {
const modelMetrics: Record<string, ModelActivityData> = {}; const modelMetrics: Record<string, ModelActivityData> = {};
dailyActivity.results.forEach((day) => { dailyActivity.results.forEach((day) => {
Object.entries(day.breakdown.models || {}).forEach(([model, modelData]) => { Object.entries(day.breakdown[key] || {}).forEach(([model, modelData]) => {
if (!modelMetrics[model]) { if (!modelMetrics[model]) {
modelMetrics[model] = { modelMetrics[model] = {
label: key === 'api_keys'
? formatKeyLabel(modelData as KeyMetricWithMetadata, model)
: model,
total_requests: 0, total_requests: 0,
total_successful_requests: 0, total_successful_requests: 0,
total_failed_requests: 0, total_failed_requests: 0,

View file

@ -49,12 +49,18 @@ interface EntitySpendData {
}; };
} }
export interface EntityList {
label: string;
value: string;
}
interface EntityUsageProps { interface EntityUsageProps {
accessToken: string | null; accessToken: string | null;
entityType: 'tag' | 'team'; entityType: 'tag' | 'team';
entityId?: string | null; entityId?: string | null;
userID: string | null; userID: string | null;
userRole: string | null; userRole: string | null;
entityList: EntityList[] | null;
} }
const EntityUsage: React.FC<EntityUsageProps> = ({ const EntityUsage: React.FC<EntityUsageProps> = ({
@ -62,7 +68,8 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
entityType, entityType,
entityId, entityId,
userID, userID,
userRole userRole,
entityList
}) => { }) => {
const [spendData, setSpendData] = useState<EntitySpendData>({ const [spendData, setSpendData] = useState<EntitySpendData>({
results: [], results: [],
@ -75,8 +82,8 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
} }
}); });
const modelMetrics = processActivityData(spendData); const modelMetrics = processActivityData(spendData, "models");
const keyMetrics = processActivityData(spendData, "api_keys");
const [selectedTags, setSelectedTags] = useState<string[]>([]); const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [dateValue, setDateValue] = useState<DateRangePickerValue>({ const [dateValue, setDateValue] = useState<DateRangePickerValue>({
from: new Date(Date.now() - 28 * 24 * 60 * 60 * 1000), from: new Date(Date.now() - 28 * 24 * 60 * 60 * 1000),
@ -225,16 +232,9 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
}; };
const getAllTags = () => { const getAllTags = () => {
const tags = new Set<string>(); if (entityList) {
spendData.results.forEach(day => { return entityList;
Object.keys(day.breakdown.entities || {}).forEach(tag => { }
tags.add(tag);
});
});
return Array.from(tags).map(tag => ({
label: tag,
value: tag
}));
}; };
const filterDataByTags = (data: EntityMetricWithMetadata[]) => { const filterDataByTags = (data: EntityMetricWithMetadata[]) => {
@ -292,9 +292,10 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
onValueChange={setDateValue} onValueChange={setDateValue}
/> />
</Col> </Col>
<Col> {entityList && entityList.length > 0 && (
<Text>Filter by {entityType === 'tag' ? 'Tags' : 'Teams'}</Text> <Col>
<Select <Text>Filter by {entityType === 'tag' ? 'Tags' : 'Teams'}</Text>
<Select
mode="multiple" mode="multiple"
style={{ width: '100%' }} style={{ width: '100%' }}
placeholder={`Select ${entityType === 'tag' ? 'tags' : 'teams'} to filter...`} placeholder={`Select ${entityType === 'tag' ? 'tags' : 'teams'} to filter...`}
@ -303,13 +304,15 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
options={getAllTags()} options={getAllTags()}
className="mt-2" className="mt-2"
allowClear allowClear
/> />
</Col> </Col>
)}
</Grid> </Grid>
<TabGroup> <TabGroup>
<TabList variant="solid" className="mt-1"> <TabList variant="solid" className="mt-1">
<Tab>Cost</Tab> <Tab>Cost</Tab>
<Tab>Activity</Tab> <Tab>Model Activity</Tab>
<Tab>Key Activity</Tab>
</TabList> </TabList>
<TabPanels> <TabPanels>
<TabPanel> <TabPanel>
@ -355,20 +358,45 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
{/* Daily Spend Chart */} {/* Daily Spend Chart */}
<Col numColSpan={2}> <Col numColSpan={2}>
<Card> <Card>
<Title>Daily Spend</Title> <Title>Daily Spend</Title>
<BarChart <BarChart
data={[...spendData.results].sort((a, b) => data={[...spendData.results].sort((a, b) =>
new Date(a.date).getTime() - new Date(b.date).getTime() new Date(a.date).getTime() - new Date(b.date).getTime()
)} )}
index="date" index="date"
categories={["metrics.spend"]} categories={["metrics.spend"]}
colors={["cyan"]} colors={["cyan"]}
valueFormatter={(value) => `$${value.toFixed(2)}`} valueFormatter={(value) => `$${value.toFixed(2)}`}
yAxisWidth={100} yAxisWidth={100}
showLegend={false} showLegend={false}
/> customTooltip={({ payload, active }) => {
</Card> if (!active || !payload?.[0]) return null;
const data = payload[0].payload;
return (
<div className="bg-white p-4 shadow-lg rounded-lg border">
<p className="font-bold">{data.date}</p>
<p className="text-cyan-500">Total Spend: ${data.metrics.spend.toFixed(2)}</p>
<p className="text-gray-600">Total Requests: {data.metrics.api_requests}</p>
<p className="text-gray-600">Successful: {data.metrics.successful_requests}</p>
<p className="text-gray-600">Failed: {data.metrics.failed_requests}</p>
<p className="text-gray-600">Total Tokens: {data.metrics.total_tokens}</p>
<div className="mt-2 border-t pt-2">
<p className="font-semibold">Spend by {entityType === 'tag' ? 'Tag' : 'Team'}:</p>
{Object.entries(data.breakdown.entities || {}).map(([entity, entityData]) => {
const metrics = entityData as EntityMetrics;
return (
<p key={entity} className="text-sm text-gray-600">
{metrics.metadata.team_alias || entity}: ${metrics.metrics.spend.toFixed(2)}
</p>
);
})}
</div>
</div>
);
}}
/>
</Card>
</Col> </Col>
{/* Entity Breakdown Section */} {/* Entity Breakdown Section */}
@ -392,7 +420,7 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
index="metadata.alias" index="metadata.alias"
categories={["metrics.spend"]} categories={["metrics.spend"]}
colors={["cyan"]} colors={["cyan"]}
valueFormatter={(value) => `$${value.toFixed(7)}`} valueFormatter={(value) => `$${value.toFixed(4)}`}
layout="vertical" layout="vertical"
showLegend={false} showLegend={false}
yAxisWidth={100} yAxisWidth={100}
@ -415,7 +443,7 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
.map((entity) => ( .map((entity) => (
<TableRow key={entity.metadata.id}> <TableRow key={entity.metadata.id}>
<TableCell>{entity.metadata.alias}</TableCell> <TableCell>{entity.metadata.alias}</TableCell>
<TableCell>${entity.metrics.spend.toFixed(7)}</TableCell> <TableCell>${entity.metrics.spend.toFixed(4)}</TableCell>
<TableCell className="text-green-600"> <TableCell className="text-green-600">
{entity.metrics.successful_requests.toLocaleString()} {entity.metrics.successful_requests.toLocaleString()}
</TableCell> </TableCell>
@ -521,6 +549,9 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
<TabPanel> <TabPanel>
<ActivityMetrics modelMetrics={modelMetrics} /> <ActivityMetrics modelMetrics={modelMetrics} />
</TabPanel> </TabPanel>
<TabPanel>
<ActivityMetrics modelMetrics={keyMetrics} />
</TabPanel>
</TabPanels> </TabPanels>
</TabGroup> </TabGroup>
</div> </div>

View file

@ -57,7 +57,7 @@ const Sidebar: React.FC<SidebarProps> = ({
{ key: "1", page: "api-keys", label: "Virtual Keys", icon: <KeyOutlined /> }, { key: "1", page: "api-keys", label: "Virtual Keys", icon: <KeyOutlined /> },
{ key: "3", page: "llm-playground", label: "Test Key", icon: <PlayCircleOutlined />, roles: rolesWithWriteAccess }, { key: "3", page: "llm-playground", label: "Test Key", icon: <PlayCircleOutlined />, roles: rolesWithWriteAccess },
{ key: "2", page: "models", label: "Models", icon: <BlockOutlined />, roles: rolesWithWriteAccess }, { key: "2", page: "models", label: "Models", icon: <BlockOutlined />, roles: rolesWithWriteAccess },
{ key: "4", page: "usage", label: "Usage", icon: <BarChartOutlined /> }, { key: "12", page: "new_usage", label: "Usage", icon: <BarChartOutlined />, roles: [...all_admin_roles, ...internalUserRoles] },
{ key: "6", page: "teams", label: "Teams", icon: <TeamOutlined /> }, { key: "6", page: "teams", label: "Teams", icon: <TeamOutlined /> },
{ key: "17", page: "organizations", label: "Organizations", icon: <BankOutlined />, roles: all_admin_roles }, { key: "17", page: "organizations", label: "Organizations", icon: <BankOutlined />, roles: all_admin_roles },
{ key: "5", page: "users", label: "Internal Users", icon: <UserOutlined />, roles: all_admin_roles }, { key: "5", page: "users", label: "Internal Users", icon: <UserOutlined />, roles: all_admin_roles },
@ -73,7 +73,7 @@ const Sidebar: React.FC<SidebarProps> = ({
{ key: "9", page: "caching", label: "Caching", icon: <DatabaseOutlined />, roles: all_admin_roles }, { key: "9", page: "caching", label: "Caching", icon: <DatabaseOutlined />, roles: all_admin_roles },
{ key: "10", page: "budgets", label: "Budgets", icon: <BankOutlined />, roles: all_admin_roles }, { key: "10", page: "budgets", label: "Budgets", icon: <BankOutlined />, roles: all_admin_roles },
{ key: "11", page: "guardrails", label: "Guardrails", icon: <SafetyOutlined />, roles: all_admin_roles }, { key: "11", page: "guardrails", label: "Guardrails", icon: <SafetyOutlined />, roles: all_admin_roles },
{ key: "12", page: "new_usage", label: "New Usage", icon: <BarChartOutlined />, roles: [...all_admin_roles, ...internalUserRoles] }, { key: "4", page: "usage", label: "Old Usage", icon: <BarChartOutlined /> },
{ key: "20", page: "transform-request", label: "API Playground", icon: <ApiOutlined />, roles: [...all_admin_roles, ...internalUserRoles] }, { key: "20", page: "transform-request", label: "API Playground", icon: <ApiOutlined />, roles: [...all_admin_roles, ...internalUserRoles] },
{ key: "18", page: "mcp-tools", label: "MCP Tools", icon: <ToolOutlined />, roles: all_admin_roles }, { key: "18", page: "mcp-tools", label: "MCP Tools", icon: <ToolOutlined />, roles: all_admin_roles },
{ key: "19", page: "tag-management", label: "Tag Management", icon: <TagsOutlined />, roles: all_admin_roles }, { key: "19", page: "tag-management", label: "Tag Management", icon: <TagsOutlined />, roles: all_admin_roles },

View file

@ -1200,6 +1200,7 @@ export const teamDailyActivityCall = async (accessToken: String, startTime: Date
if (teamIds) { if (teamIds) {
queryParams.append('team_ids', teamIds.join(',')); queryParams.append('team_ids', teamIds.join(','));
} }
queryParams.append('exclude_team_ids', 'litellm-dashboard');
const queryString = queryParams.toString(); const queryString = queryParams.toString();
if (queryString) { if (queryString) {
url += `?${queryString}`; url += `?${queryString}`;

View file

@ -17,24 +17,29 @@ import {
} from "@tremor/react"; } from "@tremor/react";
import { AreaChart } from "@tremor/react"; import { AreaChart } from "@tremor/react";
import { userDailyActivityCall } from "./networking"; import { userDailyActivityCall, tagListCall } from "./networking";
import { Tag } from "./tag_management/types";
import ViewUserSpend from "./view_user_spend"; import ViewUserSpend from "./view_user_spend";
import TopKeyView from "./top_key_view"; import TopKeyView from "./top_key_view";
import { ActivityMetrics, processActivityData } from './activity_metrics'; import { ActivityMetrics, processActivityData } from './activity_metrics';
import { SpendMetrics, DailyData, ModelActivityData, MetricWithMetadata, KeyMetricWithMetadata } from './usage/types'; import { SpendMetrics, DailyData, ModelActivityData, MetricWithMetadata, KeyMetricWithMetadata } from './usage/types';
import EntityUsage from './entity_usage'; import EntityUsage from './entity_usage';
import { old_admin_roles, v2_admin_role_names, all_admin_roles, rolesAllowedToSeeUsage, rolesWithWriteAccess, internalUserRoles } from '../utils/roles'; import { old_admin_roles, v2_admin_role_names, all_admin_roles, rolesAllowedToSeeUsage, rolesWithWriteAccess, internalUserRoles } from '../utils/roles';
import { Team } from "./key_team_helpers/key_list";
import { EntityList } from "./entity_usage";
interface NewUsagePageProps { interface NewUsagePageProps {
accessToken: string | null; accessToken: string | null;
userRole: string | null; userRole: string | null;
userID: string | null; userID: string | null;
teams: Team[];
} }
const NewUsagePage: React.FC<NewUsagePageProps> = ({ const NewUsagePage: React.FC<NewUsagePageProps> = ({
accessToken, accessToken,
userRole, userRole,
userID, userID,
teams
}) => { }) => {
const [userSpendData, setUserSpendData] = useState<{ const [userSpendData, setUserSpendData] = useState<{
results: DailyData[]; results: DailyData[];
@ -47,6 +52,23 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
to: new Date(), to: new Date(),
}); });
const [allTags, setAllTags] = useState<EntityList[]>([]);
const getAllTags = async () => {
if (!accessToken) {
return;
}
const tags = await tagListCall(accessToken);
setAllTags(Object.values(tags).map((tag: Tag) => ({
label: tag.name,
value: tag.name
})));
};
useEffect(() => {
getAllTags();
}, [accessToken]);
// Derived states from userSpendData // Derived states from userSpendData
const totalSpend = userSpendData.metadata?.total_spend || 0; const totalSpend = userSpendData.metadata?.total_spend || 0;
@ -228,16 +250,19 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
fetchUserSpendData(); fetchUserSpendData();
}, [accessToken, dateValue]); }, [accessToken, dateValue]);
const modelMetrics = processActivityData(userSpendData); const modelMetrics = processActivityData(userSpendData, "models");
const keyMetrics = processActivityData(userSpendData, "api_keys");
return ( return (
<div style={{ width: "100%" }} className="p-8"> <div style={{ width: "100%" }} className="p-8">
<Text>Usage Analytics Dashboard</Text> <Text className="text-sm text-gray-500 mb-4">
This is the new usage dashboard. <br/> You may see empty data, as these use <a href="https://github.com/BerriAI/litellm/blob/6de348125208dd4be81ff0e5813753df2fbe9735/schema.prisma#L320" className="text-blue-500 hover:text-blue-700 ml-1">new aggregate tables</a> to allow UI to work at 1M+ spend logs. To access the old dashboard, go to Experimental {'>'} Old Usage.
</Text>
<TabGroup> <TabGroup>
<TabList variant="solid" className="mt-1"> <TabList variant="solid" className="mt-1">
<Tab>Your Usage</Tab> {all_admin_roles.includes(userRole || "") ? <Tab>Global Usage</Tab> : <Tab>Your Usage</Tab>}
<Tab>Team Usage</Tab> <Tab>Team Usage</Tab>
{all_admin_roles.includes(userRole || "") && <Tab>Tag Usage</Tab>} {all_admin_roles.includes(userRole || "") ? <Tab>Tag Usage</Tab> : <></>}
</TabList> </TabList>
<TabPanels> <TabPanels>
{/* Your Usage Panel */} {/* Your Usage Panel */}
@ -257,7 +282,8 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
<TabGroup> <TabGroup>
<TabList variant="solid" className="mt-1"> <TabList variant="solid" className="mt-1">
<Tab>Cost</Tab> <Tab>Cost</Tab>
<Tab>Activity</Tab> <Tab>Model Activity</Tab>
<Tab>Key Activity</Tab>
</TabList> </TabList>
<TabPanels> <TabPanels>
{/* Cost Panel */} {/* Cost Panel */}
@ -460,6 +486,9 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
<TabPanel> <TabPanel>
<ActivityMetrics modelMetrics={modelMetrics} /> <ActivityMetrics modelMetrics={modelMetrics} />
</TabPanel> </TabPanel>
<TabPanel>
<ActivityMetrics modelMetrics={keyMetrics} />
</TabPanel>
</TabPanels> </TabPanels>
</TabGroup> </TabGroup>
</TabPanel> </TabPanel>
@ -471,6 +500,10 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
entityType="team" entityType="team"
userID={userID} userID={userID}
userRole={userRole} userRole={userRole}
entityList={teams?.map(team => ({
label: team.team_alias,
value: team.team_id
})) || null}
/> />
</TabPanel> </TabPanel>
@ -481,6 +514,7 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
entityType="tag" entityType="tag"
userID={userID} userID={userID}
userRole={userRole} userRole={userRole}
entityList={allTags}
/> />
</TabPanel> </TabPanel>

View file

@ -31,10 +31,12 @@ export interface KeyMetricWithMetadata {
metrics: SpendMetrics; metrics: SpendMetrics;
metadata: { metadata: {
key_alias: string | null; key_alias: string | null;
team_id?: string | null;
}; };
} }
export interface ModelActivityData { export interface ModelActivityData {
label: string;
total_requests: number; total_requests: number;
total_successful_requests: number; total_successful_requests: number;
total_failed_requests: number; total_failed_requests: number;
@ -62,11 +64,7 @@ export interface ModelActivityData {
export interface KeyMetadata { export interface KeyMetadata {
key_alias: string | null; key_alias: string | null;
} team_id: string | null;
export interface KeyMetricWithMetadata {
metrics: SpendMetrics;
metadata: KeyMetadata;
} }
export interface EntityMetadata { export interface EntityMetadata {