mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-25 18:54:30 +00:00
UI - New Usage Tab fixes (#9696)
* fix(new_usage.tsx): enable smooth scrolling - remove double scroll bars enables easier viewing of overflowing content * fix(new_usage.tsx): fix ordering of daily spend ensure always from earliest to latest date * feat(internal_user_endpoints.py): return key alias on `/user/daily/activity` Enables easier consumption on UI * fix(new_usage.tsx): show key alias on usage tab * feat(activity_metric.tsx): new activity panel - showing spend per model per day allows debugging if models are not being tracked * fix(top_key_view.tsx): use consistent param for selecting key
This commit is contained in:
parent
72404b1a5e
commit
aa01fb9b34
5 changed files with 512 additions and 168 deletions
|
@ -1264,16 +1264,38 @@ class SpendMetrics(BaseModel):
|
||||||
api_requests: int = Field(default=0)
|
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):
|
class BreakdownMetrics(BaseModel):
|
||||||
"""Breakdown of spend by different dimensions"""
|
"""Breakdown of spend by different dimensions"""
|
||||||
|
|
||||||
models: Dict[str, SpendMetrics] = Field(default_factory=dict) # model -> metrics
|
models: Dict[str, MetricWithMetadata] = Field(
|
||||||
providers: Dict[str, SpendMetrics] = Field(
|
|
||||||
default_factory=dict
|
default_factory=dict
|
||||||
) # provider -> metrics
|
) # model -> {metrics, metadata}
|
||||||
api_keys: Dict[str, SpendMetrics] = Field(
|
providers: Dict[str, MetricWithMetadata] = Field(
|
||||||
default_factory=dict
|
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):
|
class DailySpendData(BaseModel):
|
||||||
|
@ -1335,30 +1357,51 @@ def update_metrics(
|
||||||
|
|
||||||
|
|
||||||
def update_breakdown_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:
|
) -> BreakdownMetrics:
|
||||||
"""Updates breakdown metrics for a single record using the existing update_metrics function"""
|
"""Updates breakdown metrics for a single record using the existing update_metrics function"""
|
||||||
|
|
||||||
# Update model breakdown
|
# Update model breakdown
|
||||||
if record.model not in breakdown.models:
|
if record.model not in breakdown.models:
|
||||||
breakdown.models[record.model] = SpendMetrics()
|
breakdown.models[record.model] = MetricWithMetadata(
|
||||||
breakdown.models[record.model] = update_metrics(
|
metrics=SpendMetrics(),
|
||||||
breakdown.models[record.model], record
|
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
|
# Update provider breakdown
|
||||||
provider = record.custom_llm_provider or "unknown"
|
provider = record.custom_llm_provider or "unknown"
|
||||||
if provider not in breakdown.providers:
|
if provider not in breakdown.providers:
|
||||||
breakdown.providers[provider] = SpendMetrics()
|
breakdown.providers[provider] = MetricWithMetadata(
|
||||||
breakdown.providers[provider] = update_metrics(
|
metrics=SpendMetrics(),
|
||||||
breakdown.providers[provider], record
|
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
|
# Update api key breakdown
|
||||||
if record.api_key not in breakdown.api_keys:
|
if record.api_key not in breakdown.api_keys:
|
||||||
breakdown.api_keys[record.api_key] = SpendMetrics()
|
breakdown.api_keys[record.api_key] = KeyMetricWithMetadata(
|
||||||
breakdown.api_keys[record.api_key] = update_metrics(
|
metrics=SpendMetrics(),
|
||||||
breakdown.api_keys[record.api_key], record
|
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
|
return breakdown
|
||||||
|
@ -1456,6 +1499,24 @@ async def get_user_daily_activity(
|
||||||
LiteLLM_DailyUserSpend(**record.model_dump()) for record in daily_spend_data
|
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
|
# Process results
|
||||||
results = []
|
results = []
|
||||||
total_metrics = SpendMetrics()
|
total_metrics = SpendMetrics()
|
||||||
|
@ -1477,7 +1538,11 @@ async def get_user_daily_activity(
|
||||||
)
|
)
|
||||||
# Update breakdowns
|
# Update breakdowns
|
||||||
grouped_data[date_str]["breakdown"] = update_breakdown_metrics(
|
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
|
# Update total metrics
|
||||||
|
|
271
ui/litellm-dashboard/src/components/activity_metrics.tsx
Normal file
271
ui/litellm-dashboard/src/components/activity_metrics.tsx
Normal file
|
@ -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<string, ModelActivityData>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModelSection = ({ modelName, metrics }: { modelName: string; metrics: ModelActivityData }) => {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{/* Summary Cards */}
|
||||||
|
<Grid numItems={4} className="gap-4">
|
||||||
|
<Card>
|
||||||
|
<Text>Total Requests</Text>
|
||||||
|
<Title>{metrics.total_requests.toLocaleString()}</Title>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<Text>Total Successful Requests</Text>
|
||||||
|
<Title>{metrics.total_successful_requests.toLocaleString()}</Title>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<Text>Total Tokens</Text>
|
||||||
|
<Title>{metrics.total_tokens.toLocaleString()}</Title>
|
||||||
|
<Text>{Math.round(metrics.total_tokens / metrics.total_successful_requests)} avg per successful request</Text>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<Text>Total Spend</Text>
|
||||||
|
<Title>${metrics.total_spend.toFixed(2)}</Title>
|
||||||
|
<Text>${(metrics.total_spend / metrics.total_successful_requests).toFixed(3)} per successful request</Text>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Charts */}
|
||||||
|
<Grid numItems={2} className="gap-4">
|
||||||
|
<Card>
|
||||||
|
<Title>Total Tokens</Title>
|
||||||
|
<AreaChart
|
||||||
|
data={metrics.daily_data}
|
||||||
|
index="date"
|
||||||
|
categories={["metrics.prompt_tokens", "metrics.completion_tokens", "metrics.total_tokens"]}
|
||||||
|
colors={["blue", "cyan", "indigo"]}
|
||||||
|
valueFormatter={(number: number) => number.toLocaleString()}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Title>Requests per day</Title>
|
||||||
|
<BarChart
|
||||||
|
data={metrics.daily_data}
|
||||||
|
index="date"
|
||||||
|
categories={["metrics.api_requests"]}
|
||||||
|
colors={["blue"]}
|
||||||
|
valueFormatter={(number: number) => number.toLocaleString()}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Title>Spend per day</Title>
|
||||||
|
<BarChart
|
||||||
|
data={metrics.daily_data}
|
||||||
|
index="date"
|
||||||
|
categories={["metrics.spend"]}
|
||||||
|
colors={["green"]}
|
||||||
|
valueFormatter={(value: number) => `$${value.toFixed(2)}`}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Title>Success vs Failed Requests</Title>
|
||||||
|
<AreaChart
|
||||||
|
data={metrics.daily_data}
|
||||||
|
index="date"
|
||||||
|
categories={["metrics.successful_requests", "metrics.failed_requests"]}
|
||||||
|
colors={["emerald", "red"]}
|
||||||
|
valueFormatter={(number: number) => number.toLocaleString()}
|
||||||
|
stack
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ActivityMetrics: React.FC<ActivityMetricsProps> = ({ 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<string, {
|
||||||
|
prompt_tokens: number;
|
||||||
|
completion_tokens: number;
|
||||||
|
total_tokens: number;
|
||||||
|
api_requests: number;
|
||||||
|
spend: number;
|
||||||
|
successful_requests: number;
|
||||||
|
failed_requests: number;
|
||||||
|
}>
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* Global Summary */}
|
||||||
|
<div className="border rounded-lg p-4">
|
||||||
|
<Title>Overall Usage</Title>
|
||||||
|
<Grid numItems={4} className="gap-4 mb-4">
|
||||||
|
<Card>
|
||||||
|
<Text>Total Requests</Text>
|
||||||
|
<Title>{totalMetrics.total_requests.toLocaleString()}</Title>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<Text>Total Successful Requests</Text>
|
||||||
|
<Title>{totalMetrics.total_successful_requests.toLocaleString()}</Title>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<Text>Total Tokens</Text>
|
||||||
|
<Title>{totalMetrics.total_tokens.toLocaleString()}</Title>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<Text>Total Spend</Text>
|
||||||
|
<Title>${totalMetrics.total_spend.toFixed(2)}</Title>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid numItems={2} className="gap-4">
|
||||||
|
<Card>
|
||||||
|
<Title>Total Tokens Over Time</Title>
|
||||||
|
<AreaChart
|
||||||
|
data={sortedDailyData}
|
||||||
|
index="date"
|
||||||
|
categories={["metrics.prompt_tokens", "metrics.completion_tokens", "metrics.total_tokens"]}
|
||||||
|
colors={["blue", "cyan", "indigo"]}
|
||||||
|
valueFormatter={(number: number) => number.toLocaleString()}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<Title>Total Requests Over Time</Title>
|
||||||
|
<AreaChart
|
||||||
|
data={sortedDailyData}
|
||||||
|
index="date"
|
||||||
|
categories={["metrics.successful_requests", "metrics.failed_requests"]}
|
||||||
|
colors={["emerald", "red"]}
|
||||||
|
valueFormatter={(number: number) => number.toLocaleString()}
|
||||||
|
stack
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Individual Model Sections */}
|
||||||
|
<Collapse defaultActiveKey={modelNames[0]}>
|
||||||
|
{modelNames.map((modelName) => (
|
||||||
|
<Collapse.Panel
|
||||||
|
key={modelName}
|
||||||
|
header={
|
||||||
|
<div className="flex justify-between items-center w-full">
|
||||||
|
<Title>{modelName || 'Unknown Model'}</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ModelSection
|
||||||
|
modelName={modelName || 'Unknown Model'}
|
||||||
|
metrics={modelMetrics[modelName]}
|
||||||
|
/>
|
||||||
|
</Collapse.Panel>
|
||||||
|
))}
|
||||||
|
</Collapse>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Process data function
|
||||||
|
export const processActivityData = (dailyActivity: { results: DailyData[] }): Record<string, ModelActivityData> => {
|
||||||
|
const modelMetrics: Record<string, ModelActivityData> = {};
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
|
@ -20,34 +20,16 @@ import { AreaChart } from "@tremor/react";
|
||||||
import { userDailyActivityCall } from "./networking";
|
import { userDailyActivityCall } from "./networking";
|
||||||
import ViewUserSpend from "./view_user_spend";
|
import ViewUserSpend from "./view_user_spend";
|
||||||
import TopKeyView from "./top_key_view";
|
import TopKeyView from "./top_key_view";
|
||||||
|
import { ActivityMetrics, processActivityData } from './activity_metrics';
|
||||||
|
import { SpendMetrics, DailyData, ModelActivityData, MetricWithMetadata, KeyMetricWithMetadata } from './usage/types';
|
||||||
interface NewUsagePageProps {
|
interface NewUsagePageProps {
|
||||||
accessToken: string | null;
|
accessToken: string | null;
|
||||||
userRole: string | null;
|
userRole: string | null;
|
||||||
userID: 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<NewUsagePageProps> = ({
|
const NewUsagePage: React.FC<NewUsagePageProps> = ({
|
||||||
accessToken,
|
accessToken,
|
||||||
|
@ -64,11 +46,12 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
|
||||||
|
|
||||||
// Calculate top models from the breakdown data
|
// Calculate top models from the breakdown data
|
||||||
const getTopModels = () => {
|
const getTopModels = () => {
|
||||||
const modelSpend: { [key: string]: SpendMetrics } = {};
|
const modelSpend: { [key: string]: MetricWithMetadata } = {};
|
||||||
userSpendData.results.forEach(day => {
|
userSpendData.results.forEach(day => {
|
||||||
Object.entries(day.breakdown.models || {}).forEach(([model, metrics]) => {
|
Object.entries(day.breakdown.models || {}).forEach(([model, metrics]) => {
|
||||||
if (!modelSpend[model]) {
|
if (!modelSpend[model]) {
|
||||||
modelSpend[model] = {
|
modelSpend[model] = {
|
||||||
|
metrics: {
|
||||||
spend: 0,
|
spend: 0,
|
||||||
prompt_tokens: 0,
|
prompt_tokens: 0,
|
||||||
completion_tokens: 0,
|
completion_tokens: 0,
|
||||||
|
@ -76,26 +59,28 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
|
||||||
api_requests: 0,
|
api_requests: 0,
|
||||||
successful_requests: 0,
|
successful_requests: 0,
|
||||||
failed_requests: 0
|
failed_requests: 0
|
||||||
|
},
|
||||||
|
metadata: {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
modelSpend[model].spend += metrics.spend;
|
modelSpend[model].metrics.spend += metrics.metrics.spend;
|
||||||
modelSpend[model].prompt_tokens += metrics.prompt_tokens;
|
modelSpend[model].metrics.prompt_tokens += metrics.metrics.prompt_tokens;
|
||||||
modelSpend[model].completion_tokens += metrics.completion_tokens;
|
modelSpend[model].metrics.completion_tokens += metrics.metrics.completion_tokens;
|
||||||
modelSpend[model].total_tokens += metrics.total_tokens;
|
modelSpend[model].metrics.total_tokens += metrics.metrics.total_tokens;
|
||||||
modelSpend[model].api_requests += metrics.api_requests;
|
modelSpend[model].metrics.api_requests += metrics.metrics.api_requests;
|
||||||
modelSpend[model].successful_requests += metrics.successful_requests || 0;
|
modelSpend[model].metrics.successful_requests += metrics.metrics.successful_requests || 0;
|
||||||
modelSpend[model].failed_requests += metrics.failed_requests || 0;
|
modelSpend[model].metrics.failed_requests += metrics.metrics.failed_requests || 0;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return Object.entries(modelSpend)
|
return Object.entries(modelSpend)
|
||||||
.map(([model, metrics]) => ({
|
.map(([model, metrics]) => ({
|
||||||
key: model,
|
key: model,
|
||||||
spend: metrics.spend,
|
spend: metrics.metrics.spend,
|
||||||
requests: metrics.api_requests,
|
requests: metrics.metrics.api_requests,
|
||||||
successful_requests: metrics.successful_requests,
|
successful_requests: metrics.metrics.successful_requests,
|
||||||
failed_requests: metrics.failed_requests,
|
failed_requests: metrics.metrics.failed_requests,
|
||||||
tokens: metrics.total_tokens
|
tokens: metrics.metrics.total_tokens
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => b.spend - a.spend)
|
.sort((a, b) => b.spend - a.spend)
|
||||||
.slice(0, 5);
|
.slice(0, 5);
|
||||||
|
@ -103,11 +88,12 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
|
||||||
|
|
||||||
// Calculate provider spend from the breakdown data
|
// Calculate provider spend from the breakdown data
|
||||||
const getProviderSpend = () => {
|
const getProviderSpend = () => {
|
||||||
const providerSpend: { [key: string]: SpendMetrics } = {};
|
const providerSpend: { [key: string]: MetricWithMetadata } = {};
|
||||||
userSpendData.results.forEach(day => {
|
userSpendData.results.forEach(day => {
|
||||||
Object.entries(day.breakdown.providers || {}).forEach(([provider, metrics]) => {
|
Object.entries(day.breakdown.providers || {}).forEach(([provider, metrics]) => {
|
||||||
if (!providerSpend[provider]) {
|
if (!providerSpend[provider]) {
|
||||||
providerSpend[provider] = {
|
providerSpend[provider] = {
|
||||||
|
metrics: {
|
||||||
spend: 0,
|
spend: 0,
|
||||||
prompt_tokens: 0,
|
prompt_tokens: 0,
|
||||||
completion_tokens: 0,
|
completion_tokens: 0,
|
||||||
|
@ -115,60 +101,67 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
|
||||||
api_requests: 0,
|
api_requests: 0,
|
||||||
successful_requests: 0,
|
successful_requests: 0,
|
||||||
failed_requests: 0
|
failed_requests: 0
|
||||||
|
},
|
||||||
|
metadata: {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
providerSpend[provider].spend += metrics.spend;
|
providerSpend[provider].metrics.spend += metrics.metrics.spend;
|
||||||
providerSpend[provider].prompt_tokens += metrics.prompt_tokens;
|
providerSpend[provider].metrics.prompt_tokens += metrics.metrics.prompt_tokens;
|
||||||
providerSpend[provider].completion_tokens += metrics.completion_tokens;
|
providerSpend[provider].metrics.completion_tokens += metrics.metrics.completion_tokens;
|
||||||
providerSpend[provider].total_tokens += metrics.total_tokens;
|
providerSpend[provider].metrics.total_tokens += metrics.metrics.total_tokens;
|
||||||
providerSpend[provider].api_requests += metrics.api_requests;
|
providerSpend[provider].metrics.api_requests += metrics.metrics.api_requests;
|
||||||
providerSpend[provider].successful_requests += metrics.successful_requests || 0;
|
providerSpend[provider].metrics.successful_requests += metrics.metrics.successful_requests || 0;
|
||||||
providerSpend[provider].failed_requests += metrics.failed_requests || 0;
|
providerSpend[provider].metrics.failed_requests += metrics.metrics.failed_requests || 0;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return Object.entries(providerSpend)
|
return Object.entries(providerSpend)
|
||||||
.map(([provider, metrics]) => ({
|
.map(([provider, metrics]) => ({
|
||||||
provider,
|
provider,
|
||||||
spend: metrics.spend,
|
spend: metrics.metrics.spend,
|
||||||
requests: metrics.api_requests,
|
requests: metrics.metrics.api_requests,
|
||||||
successful_requests: metrics.successful_requests,
|
successful_requests: metrics.metrics.successful_requests,
|
||||||
failed_requests: metrics.failed_requests,
|
failed_requests: metrics.metrics.failed_requests,
|
||||||
tokens: metrics.total_tokens
|
tokens: metrics.metrics.total_tokens
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calculate top API keys from the breakdown data
|
// Calculate top API keys from the breakdown data
|
||||||
const getTopKeys = () => {
|
const getTopKeys = () => {
|
||||||
const keySpend: { [key: string]: SpendMetrics } = {};
|
const keySpend: { [key: string]: KeyMetricWithMetadata } = {};
|
||||||
userSpendData.results.forEach(day => {
|
userSpendData.results.forEach(day => {
|
||||||
Object.entries(day.breakdown.api_keys || {}).forEach(([key, metrics]) => {
|
Object.entries(day.breakdown.api_keys || {}).forEach(([key, metrics]) => {
|
||||||
if (!keySpend[key]) {
|
if (!keySpend[key]) {
|
||||||
keySpend[key] = {
|
keySpend[key] = {
|
||||||
|
metrics: {
|
||||||
spend: 0,
|
spend: 0,
|
||||||
prompt_tokens: 0,
|
prompt_tokens: 0,
|
||||||
completion_tokens: 0,
|
completion_tokens: 0,
|
||||||
total_tokens: 0,
|
total_tokens: 0,
|
||||||
api_requests: 0,
|
api_requests: 0,
|
||||||
successful_requests: 0,
|
successful_requests: 0,
|
||||||
failed_requests: 0
|
failed_requests: 0,
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
key_alias: metrics.metadata.key_alias
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
keySpend[key].spend += metrics.spend;
|
keySpend[key].metrics.spend += metrics.metrics.spend;
|
||||||
keySpend[key].prompt_tokens += metrics.prompt_tokens;
|
keySpend[key].metrics.prompt_tokens += metrics.metrics.prompt_tokens;
|
||||||
keySpend[key].completion_tokens += metrics.completion_tokens;
|
keySpend[key].metrics.completion_tokens += metrics.metrics.completion_tokens;
|
||||||
keySpend[key].total_tokens += metrics.total_tokens;
|
keySpend[key].metrics.total_tokens += metrics.metrics.total_tokens;
|
||||||
keySpend[key].api_requests += metrics.api_requests;
|
keySpend[key].metrics.api_requests += metrics.metrics.api_requests;
|
||||||
keySpend[key].successful_requests += metrics.successful_requests;
|
keySpend[key].metrics.successful_requests += metrics.metrics.successful_requests;
|
||||||
keySpend[key].failed_requests += metrics.failed_requests;
|
keySpend[key].metrics.failed_requests += metrics.metrics.failed_requests;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return Object.entries(keySpend)
|
return Object.entries(keySpend)
|
||||||
.map(([api_key, metrics]) => ({
|
.map(([api_key, metrics]) => ({
|
||||||
api_key,
|
api_key,
|
||||||
key_alias: api_key.substring(0, 10), // Using truncated key as alias
|
key_alias: metrics.metadata.key_alias || "-", // Using truncated key as alias
|
||||||
spend: metrics.spend,
|
spend: metrics.metrics.spend,
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => b.spend - a.spend)
|
.sort((a, b) => b.spend - a.spend)
|
||||||
.slice(0, 5);
|
.slice(0, 5);
|
||||||
|
@ -186,6 +179,8 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
|
||||||
fetchUserSpendData();
|
fetchUserSpendData();
|
||||||
}, [accessToken]);
|
}, [accessToken]);
|
||||||
|
|
||||||
|
const modelMetrics = processActivityData(userSpendData);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: "100%" }} className="p-8">
|
<div style={{ width: "100%" }} className="p-8">
|
||||||
<Text>Experimental Usage page, using new `/user/daily/activity` endpoint.</Text>
|
<Text>Experimental Usage page, using new `/user/daily/activity` endpoint.</Text>
|
||||||
|
@ -197,7 +192,7 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
{/* Cost Panel */}
|
{/* Cost Panel */}
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<Grid numItems={2} className="gap-2 h-[100vh] w-full">
|
<Grid numItems={2} className="gap-2 w-full">
|
||||||
{/* Total Spend Card */}
|
{/* Total Spend Card */}
|
||||||
<Col numColSpan={2}>
|
<Col numColSpan={2}>
|
||||||
<Text className="text-tremor-default text-tremor-content dark:text-dark-tremor-content mb-2 mt-2 text-lg">
|
<Text className="text-tremor-default text-tremor-content dark:text-dark-tremor-content mb-2 mt-2 text-lg">
|
||||||
|
@ -257,7 +252,9 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
|
||||||
<Card>
|
<Card>
|
||||||
<Title>Daily Spend</Title>
|
<Title>Daily Spend</Title>
|
||||||
<BarChart
|
<BarChart
|
||||||
data={userSpendData.results}
|
data={[...userSpendData.results].sort((a, b) =>
|
||||||
|
new Date(a.date).getTime() - new Date(b.date).getTime()
|
||||||
|
)}
|
||||||
index="date"
|
index="date"
|
||||||
categories={["metrics.spend"]}
|
categories={["metrics.spend"]}
|
||||||
colors={["cyan"]}
|
colors={["cyan"]}
|
||||||
|
@ -392,77 +389,11 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
|
||||||
|
|
||||||
{/* Activity Panel */}
|
{/* Activity Panel */}
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<Grid numItems={1} className="gap-2 h-[75vh] w-full">
|
<ActivityMetrics modelMetrics={modelMetrics} />
|
||||||
<Card>
|
|
||||||
<Title>All Up</Title>
|
|
||||||
<Grid numItems={2}>
|
|
||||||
<Col>
|
|
||||||
<Subtitle style={{ fontSize: "15px", fontWeight: "normal", color: "#535452"}}>
|
|
||||||
API Requests {valueFormatterNumbers(userSpendData.metadata?.total_api_requests || 0)}
|
|
||||||
</Subtitle>
|
|
||||||
<AreaChart
|
|
||||||
className="h-40"
|
|
||||||
data={[...userSpendData.results].reverse()}
|
|
||||||
valueFormatter={valueFormatterNumbers}
|
|
||||||
index="date"
|
|
||||||
colors={['cyan']}
|
|
||||||
categories={['metrics.api_requests']}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col>
|
|
||||||
<Subtitle style={{ fontSize: "15px", fontWeight: "normal", color: "#535452"}}>
|
|
||||||
Tokens {valueFormatterNumbers(userSpendData.metadata?.total_tokens || 0)}
|
|
||||||
</Subtitle>
|
|
||||||
<BarChart
|
|
||||||
className="h-40"
|
|
||||||
data={[...userSpendData.results].reverse()}
|
|
||||||
valueFormatter={valueFormatterNumbers}
|
|
||||||
index="date"
|
|
||||||
colors={['cyan']}
|
|
||||||
categories={['metrics.total_tokens']}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Grid>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Per Model Activity */}
|
|
||||||
{Object.entries(getModelActivityData(userSpendData)).map(([model, data], index) => (
|
|
||||||
<Card key={index}>
|
|
||||||
<Title>{model}</Title>
|
|
||||||
<Grid numItems={2}>
|
|
||||||
<Col>
|
|
||||||
<Subtitle style={{ fontSize: "15px", fontWeight: "normal", color: "#535452"}}>
|
|
||||||
API Requests {valueFormatterNumbers(data.total_requests)}
|
|
||||||
</Subtitle>
|
|
||||||
<AreaChart
|
|
||||||
className="h-40"
|
|
||||||
data={[...data.daily_data].reverse()}
|
|
||||||
index="date"
|
|
||||||
colors={['cyan']}
|
|
||||||
categories={['api_requests']}
|
|
||||||
valueFormatter={valueFormatterNumbers}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col>
|
|
||||||
<Subtitle style={{ fontSize: "15px", fontWeight: "normal", color: "#535452"}}>
|
|
||||||
Tokens {valueFormatterNumbers(data.total_tokens)}
|
|
||||||
</Subtitle>
|
|
||||||
<BarChart
|
|
||||||
className="h-40"
|
|
||||||
data={data.daily_data}
|
|
||||||
index="date"
|
|
||||||
colors={['cyan']}
|
|
||||||
categories={['total_tokens']}
|
|
||||||
valueFormatter={valueFormatterNumbers}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Grid>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
</TabGroup>
|
</TabGroup>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -494,12 +425,12 @@ const getModelActivityData = (userSpendData: {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
modelData[model].total_requests += metrics.api_requests;
|
modelData[model].total_requests += metrics.metrics.api_requests;
|
||||||
modelData[model].total_tokens += metrics.total_tokens;
|
modelData[model].total_tokens += metrics.metrics.total_tokens;
|
||||||
modelData[model].daily_data.push({
|
modelData[model].daily_data.push({
|
||||||
date: day.date,
|
date: day.date,
|
||||||
api_requests: metrics.api_requests,
|
api_requests: metrics.metrics.api_requests,
|
||||||
total_tokens: metrics.total_tokens
|
total_tokens: metrics.metrics.total_tokens
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -33,8 +33,9 @@ const TopKeyView: React.FC<TopKeyViewProps> = ({
|
||||||
try {
|
try {
|
||||||
const keyInfo = await keyInfoV1Call(accessToken, item.api_key);
|
const keyInfo = await keyInfoV1Call(accessToken, item.api_key);
|
||||||
const transformedKeyData = transformKeyInfo(keyInfo);
|
const transformedKeyData = transformKeyInfo(keyInfo);
|
||||||
|
|
||||||
setKeyData(transformedKeyData);
|
setKeyData(transformedKeyData);
|
||||||
setSelectedKey(item.key);
|
setSelectedKey(item.api_key);
|
||||||
setIsModalOpen(true); // Open modal when key is clicked
|
setIsModalOpen(true); // Open modal when key is clicked
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching key info:", error);
|
console.error("Error fetching key info:", error);
|
||||||
|
@ -165,6 +166,7 @@ const TopKeyView: React.FC<TopKeyViewProps> = ({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isModalOpen && selectedKey && keyData && (
|
{isModalOpen && selectedKey && keyData && (
|
||||||
|
console.log('Rendering modal with:', { isModalOpen, selectedKey, keyData }),
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||||
onClick={handleOutsideClick}
|
onClick={handleOutsideClick}
|
||||||
|
|
75
ui/litellm-dashboard/src/components/usage/types.ts
Normal file
75
ui/litellm-dashboard/src/components/usage/types.ts
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
export interface SpendMetrics {
|
||||||
|
spend: number;
|
||||||
|
prompt_tokens: number;
|
||||||
|
completion_tokens: number;
|
||||||
|
total_tokens: number;
|
||||||
|
api_requests: number;
|
||||||
|
successful_requests: number;
|
||||||
|
failed_requests: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DailyData {
|
||||||
|
date: string;
|
||||||
|
metrics: SpendMetrics;
|
||||||
|
breakdown: BreakdownMetrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BreakdownMetrics {
|
||||||
|
models: { [key: string]: MetricWithMetadata };
|
||||||
|
providers: { [key: string]: MetricWithMetadata };
|
||||||
|
api_keys: { [key: string]: KeyMetricWithMetadata };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetricWithMetadata {
|
||||||
|
metrics: SpendMetrics;
|
||||||
|
metadata: object;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KeyMetricWithMetadata {
|
||||||
|
metrics: SpendMetrics;
|
||||||
|
metadata: {
|
||||||
|
key_alias: string | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelActivityData {
|
||||||
|
total_requests: number;
|
||||||
|
total_successful_requests: number;
|
||||||
|
total_failed_requests: number;
|
||||||
|
total_tokens: number;
|
||||||
|
prompt_tokens: number;
|
||||||
|
completion_tokens: number;
|
||||||
|
total_spend: number;
|
||||||
|
daily_data: {
|
||||||
|
date: string;
|
||||||
|
metrics: {
|
||||||
|
prompt_tokens: number;
|
||||||
|
completion_tokens: number;
|
||||||
|
total_tokens: number;
|
||||||
|
api_requests: number;
|
||||||
|
spend: number;
|
||||||
|
successful_requests: number;
|
||||||
|
failed_requests: number;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KeyMetadata {
|
||||||
|
key_alias: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KeyMetricWithMetadata {
|
||||||
|
metrics: SpendMetrics;
|
||||||
|
metadata: KeyMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetricWithMetadata {
|
||||||
|
metrics: SpendMetrics;
|
||||||
|
metadata: object;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BreakdownMetrics {
|
||||||
|
models: { [key: string]: MetricWithMetadata };
|
||||||
|
providers: { [key: string]: MetricWithMetadata };
|
||||||
|
api_keys: { [key: string]: KeyMetricWithMetadata };
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue