mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-25 02:34:29 +00:00
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:
parent
b6ffa5425a
commit
2c1d1f7193
14 changed files with 221 additions and 194 deletions
|
@ -433,14 +433,13 @@ class LiteLLMProxyRequestSetup:
|
|||
) -> Optional[List[str]]:
|
||||
tags = None
|
||||
|
||||
if llm_router and llm_router.enable_tag_filtering is True:
|
||||
# Check request headers for tags
|
||||
if "x-litellm-tags" in headers:
|
||||
if isinstance(headers["x-litellm-tags"], str):
|
||||
_tags = headers["x-litellm-tags"].split(",")
|
||||
tags = [tag.strip() for tag in _tags]
|
||||
elif isinstance(headers["x-litellm-tags"], list):
|
||||
tags = headers["x-litellm-tags"]
|
||||
# Check request headers for tags
|
||||
if "x-litellm-tags" in headers:
|
||||
if isinstance(headers["x-litellm-tags"], str):
|
||||
_tags = headers["x-litellm-tags"].split(",")
|
||||
tags = [tag.strip() for tag in _tags]
|
||||
elif isinstance(headers["x-litellm-tags"], list):
|
||||
tags = headers["x-litellm-tags"]
|
||||
# Check request body for tags
|
||||
if "tags" in data and isinstance(data["tags"], list):
|
||||
tags = data["tags"]
|
||||
|
|
|
@ -75,7 +75,8 @@ def update_breakdown_metrics(
|
|||
metadata=KeyMetadata(
|
||||
key_alias=api_key_metadata.get(record.api_key, {}).get(
|
||||
"key_alias", None
|
||||
)
|
||||
),
|
||||
team_id=api_key_metadata.get(record.api_key, {}).get("team_id", None),
|
||||
), # Add any api_key-specific metadata here
|
||||
)
|
||||
breakdown.api_keys[record.api_key].metrics = update_metrics(
|
||||
|
@ -112,6 +113,7 @@ async def get_daily_activity(
|
|||
api_key: Optional[str],
|
||||
page: int,
|
||||
page_size: int,
|
||||
exclude_entity_ids: Optional[List[str]] = None,
|
||||
) -> SpendAnalyticsPaginatedResponse:
|
||||
"""Common function to get daily activity for any entity type."""
|
||||
if prisma_client is None:
|
||||
|
@ -144,6 +146,10 @@ async def get_daily_activity(
|
|||
where_conditions[entity_id_field] = {"in": entity_id}
|
||||
else:
|
||||
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
|
||||
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)}}
|
||||
)
|
||||
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
|
||||
|
|
|
@ -25,6 +25,8 @@ from litellm._logging import verbose_proxy_logger
|
|||
from litellm.litellm_core_utils.duration_parser import duration_in_seconds
|
||||
from litellm.proxy._types import *
|
||||
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 (
|
||||
generate_key_helper_fn,
|
||||
prepare_metadata_fields,
|
||||
|
@ -1382,136 +1384,22 @@ async def get_user_daily_activity(
|
|||
)
|
||||
|
||||
try:
|
||||
# Build filter conditions
|
||||
where_conditions: Dict[str, Any] = {
|
||||
"date": {
|
||||
"gte": start_date,
|
||||
"lte": end_date,
|
||||
}
|
||||
}
|
||||
entity_id: Optional[str] = None
|
||||
if not _user_has_admin_view(user_api_key_dict):
|
||||
entity_id = user_api_key_dict.user_id
|
||||
|
||||
if model:
|
||||
where_conditions["model"] = model
|
||||
if api_key:
|
||||
where_conditions["api_key"] = api_key
|
||||
|
||||
if (
|
||||
user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN
|
||||
and user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY
|
||||
):
|
||||
where_conditions[
|
||||
"user_id"
|
||||
] = user_api_key_dict.user_id # only allow access to own data
|
||||
|
||||
# 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,
|
||||
),
|
||||
return await get_daily_activity(
|
||||
prisma_client=prisma_client,
|
||||
table_name="litellm_dailyuserspend",
|
||||
entity_id_field="user_id",
|
||||
entity_id=entity_id,
|
||||
entity_metadata_field=None,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
model=model,
|
||||
api_key=api_key,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
|
|
|
@ -12,7 +12,7 @@ All /tag management endpoints
|
|||
|
||||
import datetime
|
||||
import json
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
|
@ -25,6 +25,7 @@ from litellm.proxy.management_endpoints.common_daily_activity import (
|
|||
get_daily_activity,
|
||||
)
|
||||
from litellm.types.tag_management import (
|
||||
LiteLLM_DailyTagSpendTable,
|
||||
TagConfig,
|
||||
TagDeleteRequest,
|
||||
TagInfoRequest,
|
||||
|
@ -301,6 +302,7 @@ async def info_tag(
|
|||
"/tag/list",
|
||||
tags=["tag management"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=List[TagConfig],
|
||||
)
|
||||
async def list_tags(
|
||||
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")
|
||||
|
||||
try:
|
||||
## QUERY STORED TAGS ##
|
||||
tags_config = await _get_tags_config(prisma_client)
|
||||
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:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
|
|
@ -2102,6 +2102,7 @@ async def get_team_daily_activity(
|
|||
api_key: Optional[str] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
exclude_team_ids: Optional[str] = None,
|
||||
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.
|
||||
page (int): Page number for pagination.
|
||||
page_size (int): Number of items per page.
|
||||
|
||||
exclude_team_ids (Optional[str]): Comma-separated list of team IDs to exclude.
|
||||
Returns:
|
||||
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
|
||||
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(
|
||||
|
@ -2182,6 +2189,7 @@ async def get_team_daily_activity(
|
|||
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,
|
||||
|
|
|
@ -39,6 +39,7 @@ class KeyMetadata(BaseModel):
|
|||
"""Metadata for a key"""
|
||||
|
||||
key_alias: Optional[str] = None
|
||||
team_id: Optional[str] = None
|
||||
|
||||
|
||||
class KeyMetricWithMetadata(MetricBase):
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from datetime import datetime
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
@ -30,3 +31,23 @@ class TagDeleteRequest(BaseModel):
|
|||
|
||||
class TagInfoRequest(BaseModel):
|
||||
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
|
||||
|
|
|
@ -370,6 +370,7 @@ export default function CreateKeyPage() {
|
|||
userID={userID}
|
||||
userRole={userRole}
|
||||
accessToken={accessToken}
|
||||
teams={teams}
|
||||
/>
|
||||
) :
|
||||
(
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { Card, Grid, Text, Title, Accordion, AccordionHeader, AccordionBody } 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';
|
||||
|
||||
interface ActivityMetricsProps {
|
||||
|
@ -224,7 +224,7 @@ export const ActivityMetrics: React.FC<ActivityMetricsProps> = ({ modelMetrics }
|
|||
key={modelName}
|
||||
header={
|
||||
<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">
|
||||
<span>${modelMetrics[modelName].total_spend.toFixed(2)}</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
|
||||
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> = {};
|
||||
|
||||
dailyActivity.results.forEach((day) => {
|
||||
Object.entries(day.breakdown.models || {}).forEach(([model, modelData]) => {
|
||||
Object.entries(day.breakdown[key] || {}).forEach(([model, modelData]) => {
|
||||
if (!modelMetrics[model]) {
|
||||
modelMetrics[model] = {
|
||||
label: key === 'api_keys'
|
||||
? formatKeyLabel(modelData as KeyMetricWithMetadata, model)
|
||||
: model,
|
||||
total_requests: 0,
|
||||
total_successful_requests: 0,
|
||||
total_failed_requests: 0,
|
||||
|
|
|
@ -49,12 +49,18 @@ interface EntitySpendData {
|
|||
};
|
||||
}
|
||||
|
||||
export interface EntityList {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface EntityUsageProps {
|
||||
accessToken: string | null;
|
||||
entityType: 'tag' | 'team';
|
||||
entityId?: string | null;
|
||||
userID: string | null;
|
||||
userRole: string | null;
|
||||
entityList: EntityList[] | null;
|
||||
}
|
||||
|
||||
const EntityUsage: React.FC<EntityUsageProps> = ({
|
||||
|
@ -62,7 +68,8 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
|
|||
entityType,
|
||||
entityId,
|
||||
userID,
|
||||
userRole
|
||||
userRole,
|
||||
entityList
|
||||
}) => {
|
||||
const [spendData, setSpendData] = useState<EntitySpendData>({
|
||||
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 [dateValue, setDateValue] = useState<DateRangePickerValue>({
|
||||
from: new Date(Date.now() - 28 * 24 * 60 * 60 * 1000),
|
||||
|
@ -225,16 +232,9 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
|
|||
};
|
||||
|
||||
const getAllTags = () => {
|
||||
const tags = new Set<string>();
|
||||
spendData.results.forEach(day => {
|
||||
Object.keys(day.breakdown.entities || {}).forEach(tag => {
|
||||
tags.add(tag);
|
||||
});
|
||||
});
|
||||
return Array.from(tags).map(tag => ({
|
||||
label: tag,
|
||||
value: tag
|
||||
}));
|
||||
if (entityList) {
|
||||
return entityList;
|
||||
}
|
||||
};
|
||||
|
||||
const filterDataByTags = (data: EntityMetricWithMetadata[]) => {
|
||||
|
@ -292,9 +292,10 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
|
|||
onValueChange={setDateValue}
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
<Text>Filter by {entityType === 'tag' ? 'Tags' : 'Teams'}</Text>
|
||||
<Select
|
||||
{entityList && entityList.length > 0 && (
|
||||
<Col>
|
||||
<Text>Filter by {entityType === 'tag' ? 'Tags' : 'Teams'}</Text>
|
||||
<Select
|
||||
mode="multiple"
|
||||
style={{ width: '100%' }}
|
||||
placeholder={`Select ${entityType === 'tag' ? 'tags' : 'teams'} to filter...`}
|
||||
|
@ -303,13 +304,15 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
|
|||
options={getAllTags()}
|
||||
className="mt-2"
|
||||
allowClear
|
||||
/>
|
||||
</Col>
|
||||
/>
|
||||
</Col>
|
||||
)}
|
||||
</Grid>
|
||||
<TabGroup>
|
||||
<TabList variant="solid" className="mt-1">
|
||||
<Tab>Cost</Tab>
|
||||
<Tab>Activity</Tab>
|
||||
<Tab>Model Activity</Tab>
|
||||
<Tab>Key Activity</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
|
@ -355,20 +358,45 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
|
|||
|
||||
{/* Daily Spend Chart */}
|
||||
<Col numColSpan={2}>
|
||||
<Card>
|
||||
<Title>Daily Spend</Title>
|
||||
<BarChart
|
||||
data={[...spendData.results].sort((a, b) =>
|
||||
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}
|
||||
/>
|
||||
</Card>
|
||||
<Card>
|
||||
<Title>Daily Spend</Title>
|
||||
<BarChart
|
||||
data={[...spendData.results].sort((a, b) =>
|
||||
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}
|
||||
customTooltip={({ payload, active }) => {
|
||||
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>
|
||||
|
||||
{/* Entity Breakdown Section */}
|
||||
|
@ -392,7 +420,7 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
|
|||
index="metadata.alias"
|
||||
categories={["metrics.spend"]}
|
||||
colors={["cyan"]}
|
||||
valueFormatter={(value) => `$${value.toFixed(7)}`}
|
||||
valueFormatter={(value) => `$${value.toFixed(4)}`}
|
||||
layout="vertical"
|
||||
showLegend={false}
|
||||
yAxisWidth={100}
|
||||
|
@ -415,7 +443,7 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
|
|||
.map((entity) => (
|
||||
<TableRow key={entity.metadata.id}>
|
||||
<TableCell>{entity.metadata.alias}</TableCell>
|
||||
<TableCell>${entity.metrics.spend.toFixed(7)}</TableCell>
|
||||
<TableCell>${entity.metrics.spend.toFixed(4)}</TableCell>
|
||||
<TableCell className="text-green-600">
|
||||
{entity.metrics.successful_requests.toLocaleString()}
|
||||
</TableCell>
|
||||
|
@ -521,6 +549,9 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
|
|||
<TabPanel>
|
||||
<ActivityMetrics modelMetrics={modelMetrics} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<ActivityMetrics modelMetrics={keyMetrics} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</div>
|
||||
|
|
|
@ -57,7 +57,7 @@ const Sidebar: React.FC<SidebarProps> = ({
|
|||
{ key: "1", page: "api-keys", label: "Virtual Keys", icon: <KeyOutlined /> },
|
||||
{ key: "3", page: "llm-playground", label: "Test Key", icon: <PlayCircleOutlined />, 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: "17", page: "organizations", label: "Organizations", icon: <BankOutlined />, 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: "10", page: "budgets", label: "Budgets", icon: <BankOutlined />, 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: "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 },
|
||||
|
|
|
@ -1200,6 +1200,7 @@ export const teamDailyActivityCall = async (accessToken: String, startTime: Date
|
|||
if (teamIds) {
|
||||
queryParams.append('team_ids', teamIds.join(','));
|
||||
}
|
||||
queryParams.append('exclude_team_ids', 'litellm-dashboard');
|
||||
const queryString = queryParams.toString();
|
||||
if (queryString) {
|
||||
url += `?${queryString}`;
|
||||
|
|
|
@ -17,24 +17,29 @@ import {
|
|||
} 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 TopKeyView from "./top_key_view";
|
||||
import { ActivityMetrics, processActivityData } from './activity_metrics';
|
||||
import { SpendMetrics, DailyData, ModelActivityData, MetricWithMetadata, KeyMetricWithMetadata } from './usage/types';
|
||||
import EntityUsage from './entity_usage';
|
||||
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 {
|
||||
accessToken: string | null;
|
||||
userRole: string | null;
|
||||
userID: string | null;
|
||||
teams: Team[];
|
||||
}
|
||||
|
||||
const NewUsagePage: React.FC<NewUsagePageProps> = ({
|
||||
accessToken,
|
||||
userRole,
|
||||
userID,
|
||||
teams
|
||||
}) => {
|
||||
const [userSpendData, setUserSpendData] = useState<{
|
||||
results: DailyData[];
|
||||
|
@ -47,6 +52,23 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
|
|||
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
|
||||
const totalSpend = userSpendData.metadata?.total_spend || 0;
|
||||
|
||||
|
@ -228,16 +250,19 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
|
|||
fetchUserSpendData();
|
||||
}, [accessToken, dateValue]);
|
||||
|
||||
const modelMetrics = processActivityData(userSpendData);
|
||||
const modelMetrics = processActivityData(userSpendData, "models");
|
||||
const keyMetrics = processActivityData(userSpendData, "api_keys");
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
{all_admin_roles.includes(userRole || "") && <Tab>Tag Usage</Tab>}
|
||||
{all_admin_roles.includes(userRole || "") ? <Tab>Tag Usage</Tab> : <></>}
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
{/* Your Usage Panel */}
|
||||
|
@ -257,7 +282,8 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
|
|||
<TabGroup>
|
||||
<TabList variant="solid" className="mt-1">
|
||||
<Tab>Cost</Tab>
|
||||
<Tab>Activity</Tab>
|
||||
<Tab>Model Activity</Tab>
|
||||
<Tab>Key Activity</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
{/* Cost Panel */}
|
||||
|
@ -460,6 +486,9 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
|
|||
<TabPanel>
|
||||
<ActivityMetrics modelMetrics={modelMetrics} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<ActivityMetrics modelMetrics={keyMetrics} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</TabPanel>
|
||||
|
@ -471,6 +500,10 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
|
|||
entityType="team"
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
entityList={teams?.map(team => ({
|
||||
label: team.team_alias,
|
||||
value: team.team_id
|
||||
})) || null}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
|
@ -481,6 +514,7 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
|
|||
entityType="tag"
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
entityList={allTags}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
|
|
|
@ -31,10 +31,12 @@ export interface KeyMetricWithMetadata {
|
|||
metrics: SpendMetrics;
|
||||
metadata: {
|
||||
key_alias: string | null;
|
||||
team_id?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ModelActivityData {
|
||||
label: string;
|
||||
total_requests: number;
|
||||
total_successful_requests: number;
|
||||
total_failed_requests: number;
|
||||
|
@ -62,11 +64,7 @@ export interface ModelActivityData {
|
|||
|
||||
export interface KeyMetadata {
|
||||
key_alias: string | null;
|
||||
}
|
||||
|
||||
export interface KeyMetricWithMetadata {
|
||||
metrics: SpendMetrics;
|
||||
metadata: KeyMetadata;
|
||||
team_id: string | null;
|
||||
}
|
||||
|
||||
export interface EntityMetadata {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue