diff --git a/litellm/proxy/management_endpoints/internal_user_endpoints.py b/litellm/proxy/management_endpoints/internal_user_endpoints.py index ec7c6740bc..5ecd210d3e 100644 --- a/litellm/proxy/management_endpoints/internal_user_endpoints.py +++ b/litellm/proxy/management_endpoints/internal_user_endpoints.py @@ -1264,16 +1264,38 @@ class SpendMetrics(BaseModel): api_requests: int = Field(default=0) +class MetricBase(BaseModel): + metrics: SpendMetrics + + +class MetricWithMetadata(MetricBase): + metadata: Dict[str, Any] = Field(default_factory=dict) + + +class KeyMetadata(BaseModel): + """Metadata for a key""" + + key_alias: Optional[str] = None + + +class KeyMetricWithMetadata(MetricBase): + """Base class for metrics with additional metadata""" + + metadata: KeyMetadata = Field(default_factory=KeyMetadata) + + class BreakdownMetrics(BaseModel): """Breakdown of spend by different dimensions""" - models: Dict[str, SpendMetrics] = Field(default_factory=dict) # model -> metrics - providers: Dict[str, SpendMetrics] = Field( + models: Dict[str, MetricWithMetadata] = Field( default_factory=dict - ) # provider -> metrics - api_keys: Dict[str, SpendMetrics] = Field( + ) # model -> {metrics, metadata} + providers: Dict[str, MetricWithMetadata] = Field( default_factory=dict - ) # api_key -> metrics + ) # provider -> {metrics, metadata} + api_keys: Dict[str, KeyMetricWithMetadata] = Field( + default_factory=dict + ) # api_key -> {metrics, metadata} class DailySpendData(BaseModel): @@ -1335,30 +1357,51 @@ def update_metrics( def update_breakdown_metrics( - breakdown: BreakdownMetrics, record: LiteLLM_DailyUserSpend + breakdown: BreakdownMetrics, + record: LiteLLM_DailyUserSpend, + model_metadata: Dict[str, Dict[str, Any]], + provider_metadata: Dict[str, Dict[str, Any]], + api_key_metadata: Dict[str, Dict[str, Any]], ) -> BreakdownMetrics: """Updates breakdown metrics for a single record using the existing update_metrics function""" # Update model breakdown if record.model not in breakdown.models: - breakdown.models[record.model] = SpendMetrics() - breakdown.models[record.model] = update_metrics( - breakdown.models[record.model], record + breakdown.models[record.model] = MetricWithMetadata( + metrics=SpendMetrics(), + metadata=model_metadata.get( + record.model, {} + ), # Add any model-specific metadata here + ) + breakdown.models[record.model].metrics = update_metrics( + breakdown.models[record.model].metrics, record ) # Update provider breakdown provider = record.custom_llm_provider or "unknown" if provider not in breakdown.providers: - breakdown.providers[provider] = SpendMetrics() - breakdown.providers[provider] = update_metrics( - breakdown.providers[provider], record + breakdown.providers[provider] = MetricWithMetadata( + metrics=SpendMetrics(), + metadata=provider_metadata.get( + provider, {} + ), # Add any provider-specific metadata here + ) + breakdown.providers[provider].metrics = update_metrics( + breakdown.providers[provider].metrics, record ) # Update api key breakdown if record.api_key not in breakdown.api_keys: - breakdown.api_keys[record.api_key] = SpendMetrics() - breakdown.api_keys[record.api_key] = update_metrics( - breakdown.api_keys[record.api_key], record + breakdown.api_keys[record.api_key] = KeyMetricWithMetadata( + metrics=SpendMetrics(), + metadata=KeyMetadata( + key_alias=api_key_metadata.get(record.api_key, {}).get( + "key_alias", None + ) + ), # Add any api_key-specific metadata here + ) + breakdown.api_keys[record.api_key].metrics = update_metrics( + breakdown.api_keys[record.api_key].metrics, record ) return breakdown @@ -1456,6 +1499,24 @@ async def get_user_daily_activity( 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() @@ -1477,7 +1538,11 @@ async def get_user_daily_activity( ) # Update breakdowns grouped_data[date_str]["breakdown"] = update_breakdown_metrics( - grouped_data[date_str]["breakdown"], record + grouped_data[date_str]["breakdown"], + record, + model_metadata, + provider_metadata, + api_key_metadata, ) # Update total metrics diff --git a/ui/litellm-dashboard/src/components/activity_metrics.tsx b/ui/litellm-dashboard/src/components/activity_metrics.tsx new file mode 100644 index 0000000000..1c999531c3 --- /dev/null +++ b/ui/litellm-dashboard/src/components/activity_metrics.tsx @@ -0,0 +1,271 @@ +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 { Collapse } from 'antd'; + +interface ActivityMetricsProps { + modelMetrics: Record; +} + +const ModelSection = ({ modelName, metrics }: { modelName: string; metrics: ModelActivityData }) => { + return ( +
+ {/* Summary Cards */} + + + Total Requests + {metrics.total_requests.toLocaleString()} + + + Total Successful Requests + {metrics.total_successful_requests.toLocaleString()} + + + Total Tokens + {metrics.total_tokens.toLocaleString()} + {Math.round(metrics.total_tokens / metrics.total_successful_requests)} avg per successful request + + + Total Spend + ${metrics.total_spend.toFixed(2)} + ${(metrics.total_spend / metrics.total_successful_requests).toFixed(3)} per successful request + + + + {/* Charts */} + + + Total Tokens + number.toLocaleString()} + /> + + + + Requests per day + number.toLocaleString()} + /> + + + + Spend per day + `$${value.toFixed(2)}`} + /> + + + + Success vs Failed Requests + number.toLocaleString()} + stack + /> + + +
+ ); +}; + +export const ActivityMetrics: React.FC = ({ modelMetrics }) => { + const modelNames = Object.keys(modelMetrics).sort((a, b) => { + if (a === '') return 1; + if (b === '') return -1; + return modelMetrics[b].total_spend - modelMetrics[a].total_spend; + }); + + // Calculate total metrics across all models + const totalMetrics = { + total_requests: 0, + total_successful_requests: 0, + total_tokens: 0, + total_spend: 0, + daily_data: {} as Record + }; + + // Aggregate data + Object.values(modelMetrics).forEach(model => { + totalMetrics.total_requests += model.total_requests; + totalMetrics.total_successful_requests += model.total_successful_requests; + totalMetrics.total_tokens += model.total_tokens; + totalMetrics.total_spend += model.total_spend; + + // Aggregate daily data + model.daily_data.forEach(day => { + if (!totalMetrics.daily_data[day.date]) { + totalMetrics.daily_data[day.date] = { + prompt_tokens: 0, + completion_tokens: 0, + total_tokens: 0, + api_requests: 0, + spend: 0, + successful_requests: 0, + failed_requests: 0 + }; + } + totalMetrics.daily_data[day.date].prompt_tokens += day.metrics.prompt_tokens; + totalMetrics.daily_data[day.date].completion_tokens += day.metrics.completion_tokens; + totalMetrics.daily_data[day.date].total_tokens += day.metrics.total_tokens; + totalMetrics.daily_data[day.date].api_requests += day.metrics.api_requests; + totalMetrics.daily_data[day.date].spend += day.metrics.spend; + totalMetrics.daily_data[day.date].successful_requests += day.metrics.successful_requests; + totalMetrics.daily_data[day.date].failed_requests += day.metrics.failed_requests; + }); + }); + + // Convert daily_data object to array and sort by date + const sortedDailyData = Object.entries(totalMetrics.daily_data) + .map(([date, metrics]) => ({ date, metrics })) + .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); + + return ( +
+ {/* Global Summary */} +
+ Overall Usage + + + Total Requests + {totalMetrics.total_requests.toLocaleString()} + + + Total Successful Requests + {totalMetrics.total_successful_requests.toLocaleString()} + + + Total Tokens + {totalMetrics.total_tokens.toLocaleString()} + + + Total Spend + ${totalMetrics.total_spend.toFixed(2)} + + + + + + Total Tokens Over Time + number.toLocaleString()} + /> + + + Total Requests Over Time + number.toLocaleString()} + stack + /> + + +
+ + {/* Individual Model Sections */} + + {modelNames.map((modelName) => ( + + {modelName || 'Unknown Model'} +
+ ${modelMetrics[modelName].total_spend.toFixed(2)} + {modelMetrics[modelName].total_requests.toLocaleString()} requests +
+
+ } + > + + + ))} + + + ); +}; + +// Process data function +export const processActivityData = (dailyActivity: { results: DailyData[] }): Record => { + const modelMetrics: Record = {}; + + dailyActivity.results.forEach((day) => { + Object.entries(day.breakdown.models || {}).forEach(([model, modelData]) => { + if (!modelMetrics[model]) { + modelMetrics[model] = { + total_requests: 0, + total_successful_requests: 0, + total_failed_requests: 0, + total_tokens: 0, + prompt_tokens: 0, + completion_tokens: 0, + total_spend: 0, + daily_data: [] + }; + } + + // Update totals + modelMetrics[model].total_requests += modelData.metrics.api_requests; + modelMetrics[model].prompt_tokens += modelData.metrics.prompt_tokens; + modelMetrics[model].completion_tokens += modelData.metrics.completion_tokens; + modelMetrics[model].total_tokens += modelData.metrics.total_tokens; + modelMetrics[model].total_spend += modelData.metrics.spend; + modelMetrics[model].total_successful_requests += modelData.metrics.successful_requests; + modelMetrics[model].total_failed_requests += modelData.metrics.failed_requests; + + // Add daily data + modelMetrics[model].daily_data.push({ + date: day.date, + metrics: { + prompt_tokens: modelData.metrics.prompt_tokens, + completion_tokens: modelData.metrics.completion_tokens, + total_tokens: modelData.metrics.total_tokens, + api_requests: modelData.metrics.api_requests, + spend: modelData.metrics.spend, + successful_requests: modelData.metrics.successful_requests, + failed_requests: modelData.metrics.failed_requests + } + }); + }); + }); + + // Sort daily data + Object.values(modelMetrics).forEach(metrics => { + metrics.daily_data.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); + }); + + return modelMetrics; +}; \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/new_usage.tsx b/ui/litellm-dashboard/src/components/new_usage.tsx index 547d4a13d4..a5a0ef6a3d 100644 --- a/ui/litellm-dashboard/src/components/new_usage.tsx +++ b/ui/litellm-dashboard/src/components/new_usage.tsx @@ -20,34 +20,16 @@ import { AreaChart } from "@tremor/react"; import { userDailyActivityCall } from "./networking"; 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'; interface NewUsagePageProps { accessToken: string | null; userRole: string | null; userID: string | null; } -interface SpendMetrics { - spend: number; - prompt_tokens: number; - completion_tokens: number; - total_tokens: number; - api_requests: number; - successful_requests: number; - failed_requests: number; -} -interface BreakdownMetrics { - models: { [key: string]: SpendMetrics }; - providers: { [key: string]: SpendMetrics }; - api_keys: { [key: string]: SpendMetrics }; -} -interface DailyData { - date: string; - metrics: SpendMetrics; - breakdown: BreakdownMetrics; -} const NewUsagePage: React.FC = ({ accessToken, @@ -64,38 +46,41 @@ const NewUsagePage: React.FC = ({ // Calculate top models from the breakdown data const getTopModels = () => { - const modelSpend: { [key: string]: SpendMetrics } = {}; + const modelSpend: { [key: string]: MetricWithMetadata } = {}; userSpendData.results.forEach(day => { Object.entries(day.breakdown.models || {}).forEach(([model, metrics]) => { if (!modelSpend[model]) { modelSpend[model] = { - spend: 0, - prompt_tokens: 0, - completion_tokens: 0, - total_tokens: 0, - api_requests: 0, - successful_requests: 0, - failed_requests: 0 + metrics: { + spend: 0, + prompt_tokens: 0, + completion_tokens: 0, + total_tokens: 0, + api_requests: 0, + successful_requests: 0, + failed_requests: 0 + }, + metadata: {} }; } - modelSpend[model].spend += metrics.spend; - modelSpend[model].prompt_tokens += metrics.prompt_tokens; - modelSpend[model].completion_tokens += metrics.completion_tokens; - modelSpend[model].total_tokens += metrics.total_tokens; - modelSpend[model].api_requests += metrics.api_requests; - modelSpend[model].successful_requests += metrics.successful_requests || 0; - modelSpend[model].failed_requests += metrics.failed_requests || 0; + modelSpend[model].metrics.spend += metrics.metrics.spend; + modelSpend[model].metrics.prompt_tokens += metrics.metrics.prompt_tokens; + modelSpend[model].metrics.completion_tokens += metrics.metrics.completion_tokens; + modelSpend[model].metrics.total_tokens += metrics.metrics.total_tokens; + modelSpend[model].metrics.api_requests += metrics.metrics.api_requests; + modelSpend[model].metrics.successful_requests += metrics.metrics.successful_requests || 0; + modelSpend[model].metrics.failed_requests += metrics.metrics.failed_requests || 0; }); }); return Object.entries(modelSpend) .map(([model, metrics]) => ({ key: model, - spend: metrics.spend, - requests: metrics.api_requests, - successful_requests: metrics.successful_requests, - failed_requests: metrics.failed_requests, - tokens: metrics.total_tokens + spend: metrics.metrics.spend, + requests: metrics.metrics.api_requests, + successful_requests: metrics.metrics.successful_requests, + failed_requests: metrics.metrics.failed_requests, + tokens: metrics.metrics.total_tokens })) .sort((a, b) => b.spend - a.spend) .slice(0, 5); @@ -103,72 +88,80 @@ const NewUsagePage: React.FC = ({ // Calculate provider spend from the breakdown data const getProviderSpend = () => { - const providerSpend: { [key: string]: SpendMetrics } = {}; + const providerSpend: { [key: string]: MetricWithMetadata } = {}; userSpendData.results.forEach(day => { Object.entries(day.breakdown.providers || {}).forEach(([provider, metrics]) => { if (!providerSpend[provider]) { providerSpend[provider] = { - spend: 0, - prompt_tokens: 0, - completion_tokens: 0, - total_tokens: 0, - api_requests: 0, - successful_requests: 0, - failed_requests: 0 + metrics: { + spend: 0, + prompt_tokens: 0, + completion_tokens: 0, + total_tokens: 0, + api_requests: 0, + successful_requests: 0, + failed_requests: 0 + }, + metadata: {} }; } - providerSpend[provider].spend += metrics.spend; - providerSpend[provider].prompt_tokens += metrics.prompt_tokens; - providerSpend[provider].completion_tokens += metrics.completion_tokens; - providerSpend[provider].total_tokens += metrics.total_tokens; - providerSpend[provider].api_requests += metrics.api_requests; - providerSpend[provider].successful_requests += metrics.successful_requests || 0; - providerSpend[provider].failed_requests += metrics.failed_requests || 0; + providerSpend[provider].metrics.spend += metrics.metrics.spend; + providerSpend[provider].metrics.prompt_tokens += metrics.metrics.prompt_tokens; + providerSpend[provider].metrics.completion_tokens += metrics.metrics.completion_tokens; + providerSpend[provider].metrics.total_tokens += metrics.metrics.total_tokens; + providerSpend[provider].metrics.api_requests += metrics.metrics.api_requests; + providerSpend[provider].metrics.successful_requests += metrics.metrics.successful_requests || 0; + providerSpend[provider].metrics.failed_requests += metrics.metrics.failed_requests || 0; }); }); return Object.entries(providerSpend) .map(([provider, metrics]) => ({ provider, - spend: metrics.spend, - requests: metrics.api_requests, - successful_requests: metrics.successful_requests, - failed_requests: metrics.failed_requests, - tokens: metrics.total_tokens + spend: metrics.metrics.spend, + requests: metrics.metrics.api_requests, + successful_requests: metrics.metrics.successful_requests, + failed_requests: metrics.metrics.failed_requests, + tokens: metrics.metrics.total_tokens })); }; // Calculate top API keys from the breakdown data const getTopKeys = () => { - const keySpend: { [key: string]: SpendMetrics } = {}; + const keySpend: { [key: string]: KeyMetricWithMetadata } = {}; userSpendData.results.forEach(day => { Object.entries(day.breakdown.api_keys || {}).forEach(([key, metrics]) => { if (!keySpend[key]) { keySpend[key] = { - spend: 0, - prompt_tokens: 0, - completion_tokens: 0, - total_tokens: 0, - api_requests: 0, - successful_requests: 0, - failed_requests: 0 + metrics: { + spend: 0, + prompt_tokens: 0, + completion_tokens: 0, + total_tokens: 0, + api_requests: 0, + successful_requests: 0, + failed_requests: 0, + }, + metadata: { + key_alias: metrics.metadata.key_alias + } }; } - keySpend[key].spend += metrics.spend; - keySpend[key].prompt_tokens += metrics.prompt_tokens; - keySpend[key].completion_tokens += metrics.completion_tokens; - keySpend[key].total_tokens += metrics.total_tokens; - keySpend[key].api_requests += metrics.api_requests; - keySpend[key].successful_requests += metrics.successful_requests; - keySpend[key].failed_requests += metrics.failed_requests; + 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; }); }); return Object.entries(keySpend) .map(([api_key, metrics]) => ({ api_key, - key_alias: api_key.substring(0, 10), // Using truncated key as alias - spend: metrics.spend, + 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); @@ -186,6 +179,8 @@ const NewUsagePage: React.FC = ({ fetchUserSpendData(); }, [accessToken]); + const modelMetrics = processActivityData(userSpendData); + return (
Experimental Usage page, using new `/user/daily/activity` endpoint. @@ -197,7 +192,7 @@ const NewUsagePage: React.FC = ({ {/* Cost Panel */} - + {/* Total Spend Card */} @@ -257,7 +252,9 @@ const NewUsagePage: React.FC = ({ Daily Spend + new Date(a.date).getTime() - new Date(b.date).getTime() + )} index="date" categories={["metrics.spend"]} colors={["cyan"]} @@ -392,77 +389,11 @@ const NewUsagePage: React.FC = ({ {/* Activity Panel */} - - - All Up - - - - API Requests {valueFormatterNumbers(userSpendData.metadata?.total_api_requests || 0)} - - - - - - Tokens {valueFormatterNumbers(userSpendData.metadata?.total_tokens || 0)} - - - - - - - {/* Per Model Activity */} - {Object.entries(getModelActivityData(userSpendData)).map(([model, data], index) => ( - - {model} - - - - API Requests {valueFormatterNumbers(data.total_requests)} - - - - - - Tokens {valueFormatterNumbers(data.total_tokens)} - - - - - - ))} - + +
); }; @@ -494,12 +425,12 @@ const getModelActivityData = (userSpendData: { }; } - modelData[model].total_requests += metrics.api_requests; - modelData[model].total_tokens += metrics.total_tokens; + modelData[model].total_requests += metrics.metrics.api_requests; + modelData[model].total_tokens += metrics.metrics.total_tokens; modelData[model].daily_data.push({ date: day.date, - api_requests: metrics.api_requests, - total_tokens: metrics.total_tokens + api_requests: metrics.metrics.api_requests, + total_tokens: metrics.metrics.total_tokens }); }); }); diff --git a/ui/litellm-dashboard/src/components/top_key_view.tsx b/ui/litellm-dashboard/src/components/top_key_view.tsx index d6643721ad..3c9b53f8d0 100644 --- a/ui/litellm-dashboard/src/components/top_key_view.tsx +++ b/ui/litellm-dashboard/src/components/top_key_view.tsx @@ -33,8 +33,9 @@ const TopKeyView: React.FC = ({ try { const keyInfo = await keyInfoV1Call(accessToken, item.api_key); const transformedKeyData = transformKeyInfo(keyInfo); + setKeyData(transformedKeyData); - setSelectedKey(item.key); + setSelectedKey(item.api_key); setIsModalOpen(true); // Open modal when key is clicked } catch (error) { console.error("Error fetching key info:", error); @@ -165,6 +166,7 @@ const TopKeyView: React.FC = ({ )} {isModalOpen && selectedKey && keyData && ( + console.log('Rendering modal with:', { isModalOpen, selectedKey, keyData }),