Spaces:
Running
Running
/** | |
* New Usage Page | |
* | |
* Uses the new `/user/daily/activity` endpoint to get daily activity data for a user. | |
* | |
* Works at 1m+ spend logs, by querying an aggregate table instead. | |
*/ | |
import React, { useState, useEffect } from "react"; | |
import { | |
BarChart, Card, Title, Text, | |
Grid, Col, TabGroup, TabList, Tab, | |
TabPanel, TabPanels, DonutChart, | |
Table, TableHead, TableRow, | |
TableHeaderCell, TableBody, TableCell, | |
Subtitle, DateRangePicker, DateRangePickerValue | |
} from "@tremor/react"; | |
import { AreaChart } from "@tremor/react"; | |
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[]; | |
metadata: any; | |
}>({ results: [], metadata: {} }); | |
// Add date range state | |
const [dateValue, setDateValue] = useState<DateRangePickerValue>({ | |
from: new Date(Date.now() - 28 * 24 * 60 * 60 * 1000), | |
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; | |
// Calculate top models from the breakdown data | |
const getTopModels = () => { | |
const modelSpend: { [key: string]: MetricWithMetadata } = {}; | |
userSpendData.results.forEach(day => { | |
Object.entries(day.breakdown.models || {}).forEach(([model, metrics]) => { | |
if (!modelSpend[model]) { | |
modelSpend[model] = { | |
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: {} | |
}; | |
} | |
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; | |
modelSpend[model].metrics.cache_read_input_tokens += metrics.metrics.cache_read_input_tokens || 0; | |
modelSpend[model].metrics.cache_creation_input_tokens += metrics.metrics.cache_creation_input_tokens || 0; | |
}); | |
}); | |
return Object.entries(modelSpend) | |
.map(([model, metrics]) => ({ | |
key: model, | |
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); | |
}; | |
// Calculate provider spend from the breakdown data | |
const getProviderSpend = () => { | |
const providerSpend: { [key: string]: MetricWithMetadata } = {}; | |
userSpendData.results.forEach(day => { | |
Object.entries(day.breakdown.providers || {}).forEach(([provider, metrics]) => { | |
if (!providerSpend[provider]) { | |
providerSpend[provider] = { | |
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: {} | |
}; | |
} | |
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; | |
providerSpend[provider].metrics.cache_read_input_tokens += metrics.metrics.cache_read_input_tokens || 0; | |
providerSpend[provider].metrics.cache_creation_input_tokens += metrics.metrics.cache_creation_input_tokens || 0; | |
}); | |
}); | |
return Object.entries(providerSpend) | |
.map(([provider, metrics]) => ({ | |
provider, | |
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]: KeyMetricWithMetadata } = {}; | |
userSpendData.results.forEach(day => { | |
Object.entries(day.breakdown.api_keys || {}).forEach(([key, metrics]) => { | |
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 | |
} | |
}; | |
} | |
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.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); | |
}; | |
const fetchUserSpendData = async () => { | |
if (!accessToken || !dateValue.from || !dateValue.to) return; | |
const startTime = dateValue.from; | |
const endTime = dateValue.to; | |
try { | |
// Get first page | |
const firstPageData = await userDailyActivityCall(accessToken, startTime, endTime); | |
// Check if we need to fetch more pages | |
if (firstPageData.metadata.total_pages > 10) { | |
throw new Error("Too many pages of data (>10). Please select a smaller date range."); | |
} | |
// If only one page, just set the data | |
if (firstPageData.metadata.total_pages === 1) { | |
setUserSpendData(firstPageData); | |
return; | |
} | |
// Fetch all pages | |
const allResults = [...firstPageData.results]; | |
for (let page = 2; page <= firstPageData.metadata.total_pages; page++) { | |
const pageData = await userDailyActivityCall(accessToken, startTime, endTime, page); | |
allResults.push(...pageData.results); | |
} | |
// Combine all results with the first page's metadata | |
setUserSpendData({ | |
results: allResults, | |
metadata: firstPageData.metadata | |
}); | |
} catch (error) { | |
console.error("Error fetching user spend data:", error); | |
throw error; | |
} | |
}; | |
useEffect(() => { | |
fetchUserSpendData(); | |
}, [accessToken, dateValue]); | |
const modelMetrics = processActivityData(userSpendData, "models"); | |
const keyMetrics = processActivityData(userSpendData, "api_keys"); | |
return ( | |
<div style={{ width: "100%" }} className="p-8"> | |
<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"> | |
{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 */} | |
<TabPanel> | |
<Grid numItems={2} className="gap-2 w-full mb-4"> | |
<Col> | |
<Text>Select Time Range</Text> | |
<DateRangePicker | |
enableSelect={true} | |
value={dateValue} | |
onValueChange={(value) => { | |
setDateValue(value); | |
}} | |
/> | |
</Col> | |
</Grid> | |
<TabGroup> | |
<TabList variant="solid" className="mt-1"> | |
<Tab>Cost</Tab> | |
<Tab>Model Activity</Tab> | |
<Tab>Key Activity</Tab> | |
</TabList> | |
<TabPanels> | |
{/* Cost Panel */} | |
<TabPanel> | |
<Grid numItems={2} className="gap-2 w-full"> | |
{/* Total Spend Card */} | |
<Col numColSpan={2}> | |
<Text className="text-tremor-default text-tremor-content dark:text-dark-tremor-content mb-2 mt-2 text-lg"> | |
Project Spend {new Date().toLocaleString('default', { month: 'long' })} 1 - {new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0).getDate()} | |
</Text> | |
<ViewUserSpend | |
userID={userID} | |
userRole={userRole} | |
accessToken={accessToken} | |
userSpend={totalSpend} | |
selectedTeam={null} | |
userMaxBudget={null} | |
/> | |
</Col> | |
<Col numColSpan={2}> | |
<Card> | |
<Title>Usage Metrics</Title> | |
<Grid numItems={5} className="gap-4 mt-4"> | |
<Card> | |
<Title>Total Requests</Title> | |
<Text className="text-2xl font-bold mt-2"> | |
{userSpendData.metadata?.total_api_requests?.toLocaleString() || 0} | |
</Text> | |
</Card> | |
<Card> | |
<Title>Successful Requests</Title> | |
<Text className="text-2xl font-bold mt-2 text-green-600"> | |
{userSpendData.metadata?.total_successful_requests?.toLocaleString() || 0} | |
</Text> | |
</Card> | |
<Card> | |
<Title>Failed Requests</Title> | |
<Text className="text-2xl font-bold mt-2 text-red-600"> | |
{userSpendData.metadata?.total_failed_requests?.toLocaleString() || 0} | |
</Text> | |
</Card> | |
<Card> | |
<Title>Total Tokens</Title> | |
<Text className="text-2xl font-bold mt-2"> | |
{userSpendData.metadata?.total_tokens?.toLocaleString() || 0} | |
</Text> | |
</Card> | |
<Card> | |
<Title>Average Cost per Request</Title> | |
<Text className="text-2xl font-bold mt-2"> | |
${((totalSpend || 0) / (userSpendData.metadata?.total_api_requests || 1)).toFixed(4)} | |
</Text> | |
</Card> | |
</Grid> | |
</Card> | |
</Col> | |
{/* Daily Spend Chart */} | |
<Col numColSpan={2}> | |
<Card> | |
<Title>Daily Spend</Title> | |
<BarChart | |
data={[...userSpendData.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">Spend: ${data.metrics.spend.toFixed(2)}</p> | |
<p className="text-gray-600">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">Tokens: {data.metrics.total_tokens}</p> | |
</div> | |
); | |
}} | |
/> | |
</Card> | |
</Col> | |
{/* Top API Keys */} | |
<Col numColSpan={1}> | |
<Card className="h-full"> | |
<Title>Top API Keys</Title> | |
<TopKeyView | |
topKeys={getTopKeys()} | |
accessToken={accessToken} | |
userID={userID} | |
userRole={userRole} | |
teams={null} | |
/> | |
</Card> | |
</Col> | |
{/* Top Models */} | |
<Col numColSpan={1}> | |
<Card className="h-full"> | |
<div className="flex justify-between items-center mb-4"> | |
<Title>Top Models</Title> | |
</div> | |
<BarChart | |
className="mt-4 h-40" | |
data={getTopModels()} | |
index="key" | |
categories={["spend"]} | |
colors={["cyan"]} | |
valueFormatter={(value) => `$${value.toFixed(2)}`} | |
layout="vertical" | |
yAxisWidth={200} | |
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.key}</p> | |
<p className="text-cyan-500">Spend: ${data.spend.toFixed(2)}</p> | |
<p className="text-gray-600">Total Requests: {data.requests.toLocaleString()}</p> | |
<p className="text-green-600">Successful: {data.successful_requests.toLocaleString()}</p> | |
<p className="text-red-600">Failed: {data.failed_requests.toLocaleString()}</p> | |
<p className="text-gray-600">Tokens: {data.tokens.toLocaleString()}</p> | |
</div> | |
); | |
}} | |
/> | |
</Card> | |
</Col> | |
{/* Spend by Provider */} | |
<Col numColSpan={2}> | |
<Card className="h-full"> | |
<div className="flex justify-between items-center mb-4"> | |
<Title>Spend by Provider</Title> | |
</div> | |
<Grid numItems={2}> | |
<Col numColSpan={1}> | |
<DonutChart | |
className="mt-4 h-40" | |
data={getProviderSpend()} | |
index="provider" | |
category="spend" | |
valueFormatter={(value) => `$${value.toFixed(2)}`} | |
colors={["cyan"]} | |
/> | |
</Col> | |
<Col numColSpan={1}> | |
<Table> | |
<TableHead> | |
<TableRow> | |
<TableHeaderCell>Provider</TableHeaderCell> | |
<TableHeaderCell>Spend</TableHeaderCell> | |
<TableHeaderCell className="text-green-600">Successful</TableHeaderCell> | |
<TableHeaderCell className="text-red-600">Failed</TableHeaderCell> | |
<TableHeaderCell>Tokens</TableHeaderCell> | |
</TableRow> | |
</TableHead> | |
<TableBody> | |
{getProviderSpend() | |
.filter(provider => provider.spend > 0) | |
.map((provider) => ( | |
<TableRow key={provider.provider}> | |
<TableCell>{provider.provider}</TableCell> | |
<TableCell> | |
${provider.spend < 0.00001 | |
? "less than 0.00001" | |
: provider.spend.toFixed(2)} | |
</TableCell> | |
<TableCell className="text-green-600"> | |
{provider.successful_requests.toLocaleString()} | |
</TableCell> | |
<TableCell className="text-red-600"> | |
{provider.failed_requests.toLocaleString()} | |
</TableCell> | |
<TableCell>{provider.tokens.toLocaleString()}</TableCell> | |
</TableRow> | |
))} | |
</TableBody> | |
</Table> | |
</Col> | |
</Grid> | |
</Card> | |
</Col> | |
{/* Usage Metrics */} | |
</Grid> | |
</TabPanel> | |
{/* Activity Panel */} | |
<TabPanel> | |
<ActivityMetrics modelMetrics={modelMetrics} /> | |
</TabPanel> | |
<TabPanel> | |
<ActivityMetrics modelMetrics={keyMetrics} /> | |
</TabPanel> | |
</TabPanels> | |
</TabGroup> | |
</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> | |
); | |
}; | |
// Add this helper function to process model-specific activity data | |
const getModelActivityData = (userSpendData: { | |
results: DailyData[]; | |
metadata: any; | |
}) => { | |
const modelData: { | |
[key: string]: { | |
total_requests: number; | |
total_tokens: number; | |
daily_data: Array<{ | |
date: string; | |
api_requests: number; | |
total_tokens: number; | |
}>; | |
}; | |
} = {}; | |
userSpendData.results.forEach((day: DailyData) => { | |
Object.entries(day.breakdown.models || {}).forEach(([model, metrics]) => { | |
if (!modelData[model]) { | |
modelData[model] = { | |
total_requests: 0, | |
total_tokens: 0, | |
daily_data: [] | |
}; | |
} | |
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.metrics.api_requests, | |
total_tokens: metrics.metrics.total_tokens | |
}); | |
}); | |
}); | |
return modelData; | |
}; | |
// Add this helper function for number formatting | |
function valueFormatterNumbers(number: number) { | |
const formatter = new Intl.NumberFormat('en-US', { | |
maximumFractionDigits: 0, | |
notation: 'compact', | |
compactDisplay: 'short', | |
}); | |
return formatter.format(number); | |
} | |
export default NewUsagePage; |