Litellm dev 04 18 2025 p2 (#10157)

* fix(proxy/_types.py): allow internal user to call api playground

* fix(new_usage.tsx): cleanup tag based usage - only show for proxy admin

not clear what tags internal user should be allowed to see

* fix(team_endpoints.py): allow internal user view spend for teams they belong to

* fix(team_endpoints.py): return team alias on `/team/daily/activity` API

allows displaying team alias on ui

* fix: fix linting error

* fix(entity_usage.tsx): allow viewing top keys by team

* fix(entity_usage.tsx): show alias, if available in breakdown

allows entity alias to be easily displayed

* 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

* Allow filtering usage dashboard by team + tag (#10150)

* 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: fix linting error
This commit is contained in:
Krish Dholakia 2025-04-19 07:32:23 -07:00 committed by GitHub
parent b9756bf006
commit ef6ac42658
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 429 additions and 287 deletions

View file

@ -287,6 +287,7 @@ class LiteLLMRoutes(enum.Enum):
"/v1/models",
# token counter
"/utils/token_counter",
"/utils/transform_request",
# rerank
"/rerank",
"/v1/rerank",
@ -462,6 +463,7 @@ class LiteLLMRoutes(enum.Enum):
"/team/member_delete",
"/team/permissions_list",
"/team/permissions_update",
"/team/daily/activity",
"/model/new",
"/model/update",
"/model/delete",
@ -650,9 +652,9 @@ class GenerateRequestBase(LiteLLMPydanticObjectBase):
allowed_cache_controls: Optional[list] = []
config: Optional[dict] = {}
permissions: Optional[dict] = {}
model_max_budget: Optional[dict] = (
{}
) # {"gpt-4": 5.0, "gpt-3.5-turbo": 5.0}, defaults to {}
model_max_budget: Optional[
dict
] = {} # {"gpt-4": 5.0, "gpt-3.5-turbo": 5.0}, defaults to {}
model_config = ConfigDict(protected_namespaces=())
model_rpm_limit: Optional[dict] = None
@ -911,12 +913,12 @@ class NewCustomerRequest(BudgetNewRequest):
alias: Optional[str] = None # human-friendly alias
blocked: bool = False # allow/disallow requests for this end-user
budget_id: Optional[str] = None # give either a budget_id or max_budget
allowed_model_region: Optional[AllowedModelRegion] = (
None # require all user requests to use models in this specific region
)
default_model: Optional[str] = (
None # if no equivalent model in allowed region - default all requests to this model
)
allowed_model_region: Optional[
AllowedModelRegion
] = None # require all user requests to use models in this specific region
default_model: Optional[
str
] = None # if no equivalent model in allowed region - default all requests to this model
@model_validator(mode="before")
@classmethod
@ -938,12 +940,12 @@ class UpdateCustomerRequest(LiteLLMPydanticObjectBase):
blocked: bool = False # allow/disallow requests for this end-user
max_budget: Optional[float] = None
budget_id: Optional[str] = None # give either a budget_id or max_budget
allowed_model_region: Optional[AllowedModelRegion] = (
None # require all user requests to use models in this specific region
)
default_model: Optional[str] = (
None # if no equivalent model in allowed region - default all requests to this model
)
allowed_model_region: Optional[
AllowedModelRegion
] = None # require all user requests to use models in this specific region
default_model: Optional[
str
] = None # if no equivalent model in allowed region - default all requests to this model
class DeleteCustomerRequest(LiteLLMPydanticObjectBase):
@ -1079,9 +1081,9 @@ class BlockKeyRequest(LiteLLMPydanticObjectBase):
class AddTeamCallback(LiteLLMPydanticObjectBase):
callback_name: str
callback_type: Optional[Literal["success", "failure", "success_and_failure"]] = (
"success_and_failure"
)
callback_type: Optional[
Literal["success", "failure", "success_and_failure"]
] = "success_and_failure"
callback_vars: Dict[str, str]
@model_validator(mode="before")
@ -1339,9 +1341,9 @@ class ConfigList(LiteLLMPydanticObjectBase):
stored_in_db: Optional[bool]
field_default_value: Any
premium_field: bool = False
nested_fields: Optional[List[FieldDetail]] = (
None # For nested dictionary or Pydantic fields
)
nested_fields: Optional[
List[FieldDetail]
] = None # For nested dictionary or Pydantic fields
class ConfigGeneralSettings(LiteLLMPydanticObjectBase):
@ -1609,9 +1611,9 @@ class LiteLLM_OrganizationMembershipTable(LiteLLMPydanticObjectBase):
budget_id: Optional[str] = None
created_at: datetime
updated_at: datetime
user: Optional[Any] = (
None # You might want to replace 'Any' with a more specific type if available
)
user: Optional[
Any
] = None # You might want to replace 'Any' with a more specific type if available
litellm_budget_table: Optional[LiteLLM_BudgetTable] = None
model_config = ConfigDict(protected_namespaces=())
@ -2359,9 +2361,9 @@ class TeamModelDeleteRequest(BaseModel):
# Organization Member Requests
class OrganizationMemberAddRequest(OrgMemberAddRequest):
organization_id: str
max_budget_in_organization: Optional[float] = (
None # Users max budget within the organization
)
max_budget_in_organization: Optional[
float
] = None # Users max budget within the organization
class OrganizationMemberDeleteRequest(MemberDeleteRequest):
@ -2550,9 +2552,9 @@ class ProviderBudgetResponse(LiteLLMPydanticObjectBase):
Maps provider names to their budget configs.
"""
providers: Dict[str, ProviderBudgetResponseObject] = (
{}
) # Dictionary mapping provider names to their budget configurations
providers: Dict[
str, ProviderBudgetResponseObject
] = {} # Dictionary mapping provider names to their budget configurations
class ProxyStateVariables(TypedDict):
@ -2680,9 +2682,9 @@ class LiteLLM_JWTAuth(LiteLLMPydanticObjectBase):
enforce_rbac: bool = False
roles_jwt_field: Optional[str] = None # v2 on role mappings
role_mappings: Optional[List[RoleMapping]] = None
object_id_jwt_field: Optional[str] = (
None # can be either user / team, inferred from the role mapping
)
object_id_jwt_field: Optional[
str
] = None # can be either user / team, inferred from the role mapping
scope_mappings: Optional[List[ScopeMapping]] = None
enforce_scope_based_access: bool = False
enforce_team_based_model_access: bool = False

View file

@ -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"]

View file

@ -39,6 +39,7 @@ def update_breakdown_metrics(
provider_metadata: Dict[str, Dict[str, Any]],
api_key_metadata: Dict[str, Dict[str, Any]],
entity_id_field: Optional[str] = None,
entity_metadata_field: Optional[Dict[str, dict]] = None,
) -> BreakdownMetrics:
"""Updates breakdown metrics for a single record using the existing update_metrics function"""
@ -74,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(
@ -87,7 +89,10 @@ def update_breakdown_metrics(
if entity_value:
if entity_value not in breakdown.entities:
breakdown.entities[entity_value] = MetricWithMetadata(
metrics=SpendMetrics(), metadata={}
metrics=SpendMetrics(),
metadata=entity_metadata_field.get(entity_value, {})
if entity_metadata_field
else {},
)
breakdown.entities[entity_value].metrics = update_metrics(
breakdown.entities[entity_value].metrics, record
@ -101,12 +106,14 @@ async def get_daily_activity(
table_name: str,
entity_id_field: str,
entity_id: Optional[Union[str, List[str]]],
entity_metadata_field: Optional[Dict[str, dict]],
start_date: Optional[str],
end_date: Optional[str],
model: Optional[str],
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:
@ -139,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(
@ -170,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
@ -198,6 +212,7 @@ async def get_daily_activity(
provider_metadata,
api_key_metadata,
entity_id_field=entity_id_field,
entity_metadata_field=entity_metadata_field,
)
# Update total metrics

View file

@ -4,11 +4,19 @@ from litellm.proxy._types import (
GenerateKeyRequest,
LiteLLM_ManagementEndpoint_MetadataFields_Premium,
LiteLLM_TeamTable,
LitellmUserRoles,
UserAPIKeyAuth,
)
from litellm.proxy.utils import _premium_user_check
def _user_has_admin_view(user_api_key_dict: UserAPIKeyAuth) -> bool:
return (
user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN
or user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY
)
def _is_user_team_admin(
user_api_key_dict: UserAPIKeyAuth, team_obj: LiteLLM_TeamTable
) -> bool:

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.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:

View file

@ -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))
@ -400,6 +426,7 @@ async def get_tag_daily_activity(
table_name="litellm_dailytagspend",
entity_id_field="tag",
entity_id=tag_list,
entity_metadata_field=None,
start_date=start_date,
end_date=end_date,
model=model,

View file

@ -56,11 +56,13 @@ from litellm.proxy._types import (
from litellm.proxy.auth.auth_checks import (
allowed_route_check_inside_route,
get_team_object,
get_user_object,
)
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.management_endpoints.common_utils import (
_is_user_team_admin,
_set_object_metadata_field,
_user_has_admin_view,
)
from litellm.proxy.management_endpoints.tag_management_endpoints import (
get_daily_activity,
@ -2091,7 +2093,6 @@ async def update_team_member_permissions(
"/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,
@ -2101,6 +2102,8 @@ 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),
):
"""
Get daily activity for specific teams or all teams.
@ -2113,20 +2116,80 @@ 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.
"""
from litellm.proxy.proxy_server import prisma_client
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},
)
# 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(
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,
)
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:
# check if all team_ids are in user_info.teams
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
)
},
)
## Fetch team aliases
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
}
return await get_daily_activity(
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,

View file

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

View file

@ -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

View file

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

View file

@ -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,

View file

@ -8,8 +8,9 @@ import {
} from "@tremor/react";
import { Select } from 'antd';
import { ActivityMetrics, processActivityData } from './activity_metrics';
import { SpendMetrics, DailyData } from './usage/types';
import { DailyData, KeyMetricWithMetadata, EntityMetricWithMetadata } from './usage/types';
import { tagDailyActivityCall, teamDailyActivityCall } from './networking';
import TopKeyView from "./top_key_view";
interface EntityMetrics {
metrics: {
@ -48,16 +49,27 @@ 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> = ({
accessToken,
entityType,
entityId
entityId,
userID,
userRole,
entityList
}) => {
const [spendData, setSpendData] = useState<EntitySpendData>({
results: [],
@ -70,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),
@ -144,29 +156,46 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
.slice(0, 5);
};
const getTopApiKeys = () => {
const apiKeySpend: { [key: string]: any } = {};
const getTopAPIKeys = () => {
const keySpend: { [key: string]: KeyMetricWithMetadata } = {};
spendData.results.forEach(day => {
Object.entries(day.breakdown.api_keys || {}).forEach(([key, metrics]) => {
if (!apiKeySpend[key]) {
apiKeySpend[key] = {
key: key,
spend: 0,
requests: 0,
successful_requests: 0,
failed_requests: 0,
tokens: 0
if (!keySpend[key]) {
keySpend[key] = {
metrics: {
spend: 0,
prompt_tokens: 0,
completion_tokens: 0,
total_tokens: 0,
api_requests: 0,
successful_requests: 0,
failed_requests: 0,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0
},
metadata: {
key_alias: metrics.metadata.key_alias
}
};
}
apiKeySpend[key].spend += metrics.metrics.spend;
apiKeySpend[key].requests += metrics.metrics.api_requests;
apiKeySpend[key].successful_requests += metrics.metrics.successful_requests;
apiKeySpend[key].failed_requests += metrics.metrics.failed_requests;
apiKeySpend[key].tokens += metrics.metrics.total_tokens;
keySpend[key].metrics.spend += metrics.metrics.spend;
keySpend[key].metrics.prompt_tokens += metrics.metrics.prompt_tokens;
keySpend[key].metrics.completion_tokens += metrics.metrics.completion_tokens;
keySpend[key].metrics.total_tokens += metrics.metrics.total_tokens;
keySpend[key].metrics.api_requests += metrics.metrics.api_requests;
keySpend[key].metrics.successful_requests += metrics.metrics.successful_requests;
keySpend[key].metrics.failed_requests += metrics.metrics.failed_requests;
keySpend[key].metrics.cache_read_input_tokens += metrics.metrics.cache_read_input_tokens || 0;
keySpend[key].metrics.cache_creation_input_tokens += metrics.metrics.cache_creation_input_tokens || 0;
});
});
return Object.values(apiKeySpend)
return Object.entries(keySpend)
.map(([api_key, metrics]) => ({
api_key,
key_alias: metrics.metadata.key_alias || "-", // Using truncated key as alias
spend: metrics.metrics.spend,
}))
.sort((a, b) => b.spend - a.spend)
.slice(0, 5);
};
@ -203,47 +232,49 @@ 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: any[]) => {
const filterDataByTags = (data: EntityMetricWithMetadata[]) => {
if (selectedTags.length === 0) return data;
return data.filter(item => selectedTags.includes(item.entity));
return data.filter(item => selectedTags.includes(item.metadata.id));
};
const getEntityBreakdown = () => {
const entitySpend: { [key: string]: any } = {};
const entitySpend: { [key: string]: EntityMetricWithMetadata } = {};
spendData.results.forEach(day => {
Object.entries(day.breakdown.entities || {}).forEach(([entity, data]) => {
if (!entitySpend[entity]) {
entitySpend[entity] = {
entity,
spend: 0,
requests: 0,
successful_requests: 0,
failed_requests: 0,
tokens: 0
metrics: {
spend: 0,
prompt_tokens: 0,
completion_tokens: 0,
total_tokens: 0,
api_requests: 0,
successful_requests: 0,
failed_requests: 0,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0
},
metadata: {
alias: data.metadata.team_alias || entity,
id: entity
}
};
}
entitySpend[entity].spend += data.metrics.spend;
entitySpend[entity].requests += data.metrics.api_requests;
entitySpend[entity].successful_requests += data.metrics.successful_requests;
entitySpend[entity].failed_requests += data.metrics.failed_requests;
entitySpend[entity].tokens += data.metrics.total_tokens;
entitySpend[entity].metrics.spend += data.metrics.spend;
entitySpend[entity].metrics.api_requests += data.metrics.api_requests;
entitySpend[entity].metrics.successful_requests += data.metrics.successful_requests;
entitySpend[entity].metrics.failed_requests += data.metrics.failed_requests;
entitySpend[entity].metrics.total_tokens += data.metrics.total_tokens;
});
});
const result = Object.values(entitySpend)
.sort((a, b) => b.spend - a.spend);
.sort((a, b) => b.metrics.spend - a.metrics.spend);
return filterDataByTags(result);
};
@ -261,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...`}
@ -272,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>
@ -324,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 */}
@ -353,13 +412,13 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
</a>
</div>
</div>
<Grid numItems={2}>
<Col numColSpan={1}>
<Grid numItems={2}>
<Col numColSpan={1}>
<BarChart
className="mt-4 h-52"
data={getEntityBreakdown()}
index="entity"
categories={["spend"]}
index="metadata.alias"
categories={["metrics.spend"]}
colors={["cyan"]}
valueFormatter={(value) => `$${value.toFixed(4)}`}
layout="vertical"
@ -380,18 +439,18 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
</TableHead>
<TableBody>
{getEntityBreakdown()
.filter(entity => entity.spend > 0)
.filter(entity => entity.metrics.spend > 0)
.map((entity) => (
<TableRow key={entity.entity}>
<TableCell>{entity.entity}</TableCell>
<TableCell>${entity.spend.toFixed(4)}</TableCell>
<TableRow key={entity.metadata.id}>
<TableCell>{entity.metadata.alias}</TableCell>
<TableCell>${entity.metrics.spend.toFixed(4)}</TableCell>
<TableCell className="text-green-600">
{entity.successful_requests.toLocaleString()}
{entity.metrics.successful_requests.toLocaleString()}
</TableCell>
<TableCell className="text-red-600">
{entity.failed_requests.toLocaleString()}
{entity.metrics.failed_requests.toLocaleString()}
</TableCell>
<TableCell>{entity.tokens.toLocaleString()}</TableCell>
<TableCell>{entity.metrics.total_tokens.toLocaleString()}</TableCell>
</TableRow>
))}
</TableBody>
@ -407,17 +466,13 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
<Col numColSpan={1}>
<Card>
<Title>Top API Keys</Title>
<BarChart
className="mt-4 h-40"
data={getTopApiKeys()}
index="key"
categories={["spend"]}
colors={["cyan"]}
valueFormatter={(value) => `$${value.toFixed(2)}`}
layout="vertical"
yAxisWidth={200}
showLegend={false}
/>
<TopKeyView
topKeys={getTopAPIKeys()}
accessToken={accessToken}
userID={userID}
userRole={userRole}
teams={null}
/>
</Card>
</Col>
@ -494,6 +549,9 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
<TabPanel>
<ActivityMetrics modelMetrics={modelMetrics} />
</TabPanel>
<TabPanel>
<ActivityMetrics modelMetrics={keyMetrics} />
</TabPanel>
</TabPanels>
</TabGroup>
</div>

View file

@ -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 },

View file

@ -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}`;

View file

@ -17,23 +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[];
@ -46,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;
@ -227,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>
<Tab>Tag 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> : <></>}
</TabList>
<TabPanels>
{/* Your Usage Panel */}
@ -256,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 */}
@ -459,25 +486,38 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
<TabPanel>
<ActivityMetrics modelMetrics={modelMetrics} />
</TabPanel>
<TabPanel>
<ActivityMetrics modelMetrics={keyMetrics} />
</TabPanel>
</TabPanels>
</TabGroup>
</TabPanel>
{/* Tag Usage Panel */}
<TabPanel>
<EntityUsage
accessToken={accessToken}
entityType="tag"
/>
</TabPanel>
{/* Team Usage Panel */}
<TabPanel>
<EntityUsage
accessToken={accessToken}
entityType="team"
userID={userID}
userRole={userRole}
entityList={teams?.map(team => ({
label: team.team_alias,
value: team.team_id
})) || null}
/>
</TabPanel>
{/* Tag Usage Panel */}
<TabPanel>
<EntityUsage
accessToken={accessToken}
entityType="tag"
userID={userID}
userRole={userRole}
entityList={allTags}
/>
</TabPanel>
</TabPanels>
</TabGroup>
</div>

View file

@ -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,17 @@ export interface ModelActivityData {
export interface KeyMetadata {
key_alias: string | null;
team_id: string | null;
}
export interface KeyMetricWithMetadata {
export interface EntityMetadata {
alias: string;
id: string;
}
export interface EntityMetricWithMetadata {
metrics: SpendMetrics;
metadata: KeyMetadata;
metadata: EntityMetadata;
}
export interface MetricWithMetadata {