Improve Chart Readability for Tags (#12378)

* Enhance chart readability with short-form notation for large numbers (#11571)

* format y-axis value for total tokens

* format y-axis for the rest of the charts on model-activity

* revert changes for requests per day

* labels modified to plain text

* added plain text label for api_requests and spend

* minor

* improve keyname display (#11573)

* fix tags overlap

- no need to fix decimal points for valueFormatterSpend() bcz we're only going to use it for values on the x and y axis of a chart
- show only top 5 values of tags
- format the values on the x-axis

* format spend values to short form

* format spend to short form

* showing top 5 tags data more perceptively

* remove mock data

* remove console.log

* prettier

* prettier + remove unused functions
This commit is contained in:
tanjiro
2025-07-07 23:02:44 -07:00
committed by GitHub
parent 028461a84d
commit 3cb45ccb6a
5 changed files with 1070 additions and 441 deletions
@@ -1,15 +1,160 @@
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, MetricWithMetadata, KeyMetricWithMetadata } from './usage/types';
import { Collapse } from 'antd';
import { formatNumberWithCommas } from '@/utils/dataUtils';
import React from "react";
import {
Card,
Grid,
Text,
Title,
} from "@tremor/react";
import { AreaChart, BarChart } from "@tremor/react";
import type { CustomTooltipProps } from "@tremor/react";
import {
SpendMetrics,
DailyData,
ModelActivityData,
KeyMetricWithMetadata,
} from "./usage/types";
import { Collapse } from "antd";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import {
valueFormatter,
valueFormatterSpend,
} from "../components/usage/utils/value_formatters";
interface ActivityMetricsProps {
modelMetrics: Record<string, ModelActivityData>;
}
const ModelSection = ({ modelName, metrics }: { modelName: string; metrics: ModelActivityData }) => {
interface ChartDataPoint {
date: string;
metrics: SpendMetrics;
}
const colorNameToHex: { [key: string]: string } = {
blue: "#3b82f6",
cyan: "#06b6d4",
indigo: "#6366f1",
green: "#22c55e",
red: "#ef4444",
purple: "#8b5cf6",
};
export const CustomTooltip = ({
active,
payload,
label,
}: CustomTooltipProps) => {
if (active && payload && payload.length) {
const formatCategoryName = (name: string): string => {
return name
.replace("metrics.", "")
.replace(/_/g, " ")
.split(" ")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
};
const getRawValue = (
dataPoint: ChartDataPoint,
key: string
): number | undefined => {
// key is like "metrics.total_tokens"
const metricKey = key.substring(
key.indexOf(".") + 1
) as keyof SpendMetrics;
if (dataPoint.metrics && metricKey in dataPoint.metrics) {
return dataPoint.metrics[metricKey];
}
return undefined;
};
return (
<div className="w-56 rounded-tremor-default border border-tremor-border bg-tremor-background p-2 text-tremor-default shadow-tremor-dropdown">
<p className="text-tremor-content-strong">{label}</p>
{payload.map((item) => {
const dataKey = item.dataKey?.toString();
if (!dataKey || !item.payload) return null;
const rawValue = getRawValue(item.payload, dataKey);
const isSpend = dataKey.includes("spend");
const formattedValue =
rawValue !== undefined
? isSpend
? `$${rawValue.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
: rawValue.toLocaleString()
: "N/A";
const colorName = item.color as keyof typeof colorNameToHex;
const hexColor = colorNameToHex[colorName] || item.color;
return (
<div
key={dataKey}
className="flex items-center justify-between space-x-4"
>
<div className="flex items-center space-x-2">
<span
className={`h-2 w-2 shrink-0 rounded-full ring-2 ring-white drop-shadow-md`}
style={{ backgroundColor: hexColor }}
/>
<p className="font-medium text-tremor-content dark:text-dark-tremor-content">
{formatCategoryName(dataKey)}
</p>
</div>
<p className="font-medium text-tremor-content-emphasis dark:text-dark-tremor-content-emphasis">
{formattedValue}
</p>
</div>
);
})}
</div>
);
}
return null;
};
const CustomLegend = ({
categories,
colors,
}: {
categories: string[];
colors: string[];
}) => {
const formatCategoryName = (name: string): string => {
return name
.replace("metrics.", "")
.replace(/_/g, " ")
.split(" ")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
};
return (
<div className="flex items-center justify-end space-x-4">
{categories.map((category, idx) => {
const colorName = colors[idx] as keyof typeof colorNameToHex;
const hexColor = colorNameToHex[colorName] || colors[idx];
return (
<div key={category} className="flex items-center space-x-2">
<span
className={`h-2 w-2 shrink-0 rounded-full ring-4 ring-white`}
style={{ backgroundColor: hexColor }}
/>
<p className="text-sm text-tremor-content dark:text-dark-tremor-content">
{formatCategoryName(category)}
</p>
</div>
);
})}
</div>
);
};
const ModelSection = ({
modelName,
metrics,
}: {
modelName: string;
metrics: ModelActivityData;
}) => {
return (
<div className="space-y-2">
{/* Summary Cards */}
@@ -25,74 +170,156 @@ const ModelSection = ({ modelName, metrics }: { modelName: string; metrics: Mode
<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>
<Text>
{Math.round(
metrics.total_tokens / metrics.total_successful_requests
)}{" "}
avg per successful request
</Text>
</Card>
<Card>
<Text>Total Spend</Text>
<Title>${formatNumberWithCommas(metrics.total_spend, 2)}</Title>
<Text>${formatNumberWithCommas((metrics.total_spend / metrics.total_successful_requests), 3)} per successful request</Text>
<Text>
$
{formatNumberWithCommas(
metrics.total_spend / metrics.total_successful_requests,
3
)}{" "}
per successful request
</Text>
</Card>
</Grid>
{/* Charts */}
<Grid numItems={2} className="gap-4">
<Card>
<Title>Total Tokens</Title>
<AreaChart
<div className="flex justify-between items-center">
<Title>Total Tokens</Title>
<CustomLegend
categories={[
"metrics.prompt_tokens",
"metrics.completion_tokens",
"metrics.total_tokens",
]}
colors={["blue", "cyan", "indigo"]}
/>
</div>
<AreaChart
className="mt-4"
data={metrics.daily_data}
index="date"
categories={["metrics.prompt_tokens", "metrics.completion_tokens", "metrics.total_tokens"]}
categories={[
"metrics.prompt_tokens",
"metrics.completion_tokens",
"metrics.total_tokens",
]}
colors={["blue", "cyan", "indigo"]}
valueFormatter={(number: number) => number.toLocaleString()}
valueFormatter={valueFormatter}
customTooltip={CustomTooltip}
showLegend={false}
/>
</Card>
<Card>
<Title>Requests per day</Title>
<div className="flex justify-between items-center">
<Title>Requests per day</Title>
<CustomLegend
categories={["metrics.api_requests"]}
colors={["blue"]}
/>
</div>
<BarChart
className="mt-4"
data={metrics.daily_data}
index="date"
categories={["metrics.api_requests"]}
colors={["blue"]}
valueFormatter={(number: number) => number.toLocaleString()}
valueFormatter={valueFormatter}
customTooltip={CustomTooltip}
showLegend={false}
/>
</Card>
<Card>
<Title>Spend per day</Title>
<div className="flex justify-between items-center">
<Title>Spend per day</Title>
<CustomLegend categories={["metrics.spend"]} colors={["green"]} />
</div>
<BarChart
className="mt-4"
data={metrics.daily_data}
index="date"
categories={["metrics.spend"]}
colors={["green"]}
valueFormatter={(value: number) => `$${formatNumberWithCommas(value, 2)}`}
valueFormatter={valueFormatterSpend}
customTooltip={CustomTooltip}
showLegend={false}
/>
</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>
<Card>
<Title>Prompt Caching Metrics</Title>
<div className="mb-2">
<Text>Cache Read: {metrics.total_cache_read_input_tokens?.toLocaleString() || 0} tokens</Text>
<Text>Cache Creation: {metrics.total_cache_creation_input_tokens?.toLocaleString() || 0} tokens</Text>
<div className="flex justify-between items-center">
<Title>Success vs Failed Requests</Title>
<CustomLegend
categories={[
"metrics.successful_requests",
"metrics.failed_requests",
]}
colors={["green", "red"]}
/>
</div>
<AreaChart
className="mt-4"
data={metrics.daily_data}
index="date"
categories={["metrics.cache_read_input_tokens", "metrics.cache_creation_input_tokens"]}
categories={[
"metrics.successful_requests",
"metrics.failed_requests",
]}
colors={["green", "red"]}
valueFormatter={valueFormatter}
stack
customTooltip={CustomTooltip}
showLegend={false}
/>
</Card>
<Card>
<div className="flex justify-between items-center">
<Title>Prompt Caching Metrics</Title>
<CustomLegend
categories={[
"metrics.cache_read_input_tokens",
"metrics.cache_creation_input_tokens",
]}
colors={["cyan", "purple"]}
/>
</div>
<div className="mb-2">
<Text>
Cache Read:{" "}
{metrics.total_cache_read_input_tokens?.toLocaleString() || 0}{" "}
tokens
</Text>
<Text>
Cache Creation:{" "}
{metrics.total_cache_creation_input_tokens?.toLocaleString() || 0}{" "}
tokens
</Text>
</div>
<AreaChart
className="mt-4"
data={metrics.daily_data}
index="date"
categories={[
"metrics.cache_read_input_tokens",
"metrics.cache_creation_input_tokens",
]}
colors={["cyan", "purple"]}
valueFormatter={(number: number) => number.toLocaleString()}
valueFormatter={valueFormatter}
customTooltip={CustomTooltip}
showLegend={false}
/>
</Card>
</Grid>
@@ -100,10 +327,12 @@ const ModelSection = ({ modelName, metrics }: { modelName: string; metrics: Mode
);
};
export const ActivityMetrics: React.FC<ActivityMetricsProps> = ({ modelMetrics }) => {
export const ActivityMetrics: React.FC<ActivityMetricsProps> = ({
modelMetrics,
}) => {
const modelNames = Object.keys(modelMetrics).sort((a, b) => {
if (a === '') return 1;
if (b === '') return -1;
if (a === "") return 1;
if (b === "") return -1;
return modelMetrics[b].total_spend - modelMetrics[a].total_spend;
});
@@ -115,30 +344,35 @@ export const ActivityMetrics: React.FC<ActivityMetricsProps> = ({ modelMetrics }
total_spend: 0,
total_cache_read_input_tokens: 0,
total_cache_creation_input_tokens: 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;
cache_read_input_tokens: number;
cache_creation_input_tokens: number;
}>
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;
cache_read_input_tokens: number;
cache_creation_input_tokens: number;
}
>,
};
// Aggregate data
Object.values(modelMetrics).forEach(model => {
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;
totalMetrics.total_cache_read_input_tokens += model.total_cache_read_input_tokens || 0;
totalMetrics.total_cache_creation_input_tokens += model.total_cache_creation_input_tokens || 0;
totalMetrics.total_cache_read_input_tokens +=
model.total_cache_read_input_tokens || 0;
totalMetrics.total_cache_creation_input_tokens +=
model.total_cache_creation_input_tokens || 0;
// Aggregate daily data
model.daily_data.forEach(day => {
model.daily_data.forEach((day) => {
if (!totalMetrics.daily_data[day.date]) {
totalMetrics.daily_data[day.date] = {
prompt_tokens: 0,
@@ -149,18 +383,26 @@ export const ActivityMetrics: React.FC<ActivityMetricsProps> = ({ modelMetrics }
successful_requests: 0,
failed_requests: 0,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0
cache_creation_input_tokens: 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].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;
totalMetrics.daily_data[day.date].cache_read_input_tokens += day.metrics.cache_read_input_tokens || 0;
totalMetrics.daily_data[day.date].cache_creation_input_tokens += day.metrics.cache_creation_input_tokens || 0;
totalMetrics.daily_data[day.date].successful_requests +=
day.metrics.successful_requests;
totalMetrics.daily_data[day.date].failed_requests +=
day.metrics.failed_requests;
totalMetrics.daily_data[day.date].cache_read_input_tokens +=
day.metrics.cache_read_input_tokens || 0;
totalMetrics.daily_data[day.date].cache_creation_input_tokens +=
day.metrics.cache_creation_input_tokens || 0;
});
});
@@ -181,7 +423,9 @@ export const ActivityMetrics: React.FC<ActivityMetricsProps> = ({ modelMetrics }
</Card>
<Card>
<Text>Total Successful Requests</Text>
<Title>{totalMetrics.total_successful_requests.toLocaleString()}</Title>
<Title>
{totalMetrics.total_successful_requests.toLocaleString()}
</Title>
</Card>
<Card>
<Text>Total Tokens</Text>
@@ -189,30 +433,64 @@ export const ActivityMetrics: React.FC<ActivityMetricsProps> = ({ modelMetrics }
</Card>
<Card>
<Text>Total Spend</Text>
<Title>${formatNumberWithCommas(totalMetrics.total_spend, 2)}</Title>
<Title>
${formatNumberWithCommas(totalMetrics.total_spend, 2)}
</Title>
</Card>
</Grid>
<Grid numItems={2} className="gap-4">
<Card>
<Title>Total Tokens Over Time</Title>
<AreaChart
<div className="flex justify-between items-center">
<Title>Total Tokens Over Time</Title>
<CustomLegend
categories={[
"metrics.prompt_tokens",
"metrics.completion_tokens",
"metrics.total_tokens",
]}
colors={["blue", "cyan", "indigo"]}
/>
</div>
<AreaChart
className="mt-4"
data={sortedDailyData}
index="date"
categories={["metrics.prompt_tokens", "metrics.completion_tokens", "metrics.total_tokens"]}
categories={[
"metrics.prompt_tokens",
"metrics.completion_tokens",
"metrics.total_tokens",
]}
colors={["blue", "cyan", "indigo"]}
valueFormatter={(number: number) => number.toLocaleString()}
valueFormatter={valueFormatter}
customTooltip={CustomTooltip}
showLegend={false}
/>
</Card>
<Card>
<Title>Total Requests Over Time</Title>
<div className="flex justify-between items-center">
<Title>Total Requests Over Time</Title>
<CustomLegend
categories={[
"metrics.successful_requests",
"metrics.failed_requests",
]}
colors={["green", "red"]}
/>
</div>
<AreaChart
className="mt-4"
data={sortedDailyData}
index="date"
categories={["metrics.successful_requests", "metrics.failed_requests"]}
colors={["emerald", "red"]}
valueFormatter={(number: number) => number.toLocaleString()}
categories={[
"metrics.successful_requests",
"metrics.failed_requests",
]}
colors={["green", "red"]}
valueFormatter={valueFormatter}
stack
customTooltip={CustomTooltip}
showLegend={false}
/>
</Card>
</Grid>
@@ -221,21 +499,30 @@ export const ActivityMetrics: React.FC<ActivityMetricsProps> = ({ modelMetrics }
{/* Individual Model Sections */}
<Collapse defaultActiveKey={modelNames[0]}>
{modelNames.map((modelName) => (
<Collapse.Panel
key={modelName}
<Collapse.Panel
key={modelName}
header={
<div className="flex justify-between items-center w-full">
<Title>{modelMetrics[modelName].label || 'Unknown Item'}</Title>
<Title>{modelMetrics[modelName].label || "Unknown Item"}</Title>
<div className="flex space-x-4 text-sm text-gray-500">
<span>${formatNumberWithCommas(modelMetrics[modelName].total_spend, 2)}</span>
<span>{modelMetrics[modelName].total_requests.toLocaleString()} requests</span>
<span>
$
{formatNumberWithCommas(
modelMetrics[modelName].total_spend,
2
)}
</span>
<span>
{modelMetrics[modelName].total_requests.toLocaleString()}{" "}
requests
</span>
</div>
</div>
}
>
<ModelSection
modelName={modelName || 'Unknown Model'}
metrics={modelMetrics[modelName]}
<ModelSection
modelName={modelName || "Unknown Model"}
metrics={modelMetrics[modelName]}
/>
</Collapse.Panel>
))}
@@ -245,23 +532,30 @@ export const ActivityMetrics: React.FC<ActivityMetricsProps> = ({ modelMetrics }
};
// Helper function to format key label
const formatKeyLabel = (modelData: KeyMetricWithMetadata, model: string): string => {
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[] }, key: "models" | "api_keys"): 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[key] || {}).forEach(([model, modelData]) => {
if (!modelMetrics[model]) {
modelMetrics[model] = {
label: key === 'api_keys'
? formatKeyLabel(modelData as KeyMetricWithMetadata, model)
: model,
label:
key === "api_keys"
? formatKeyLabel(modelData as KeyMetricWithMetadata, model)
: model,
total_requests: 0,
total_successful_requests: 0,
total_failed_requests: 0,
@@ -271,20 +565,25 @@ export const processActivityData = (dailyActivity: { results: DailyData[] }, key
total_spend: 0,
total_cache_read_input_tokens: 0,
total_cache_creation_input_tokens: 0,
daily_data: []
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].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;
modelMetrics[model].total_cache_read_input_tokens += modelData.metrics.cache_read_input_tokens || 0;
modelMetrics[model].total_cache_creation_input_tokens += modelData.metrics.cache_creation_input_tokens || 0;
modelMetrics[model].total_successful_requests +=
modelData.metrics.successful_requests;
modelMetrics[model].total_failed_requests +=
modelData.metrics.failed_requests;
modelMetrics[model].total_cache_read_input_tokens +=
modelData.metrics.cache_read_input_tokens || 0;
modelMetrics[model].total_cache_creation_input_tokens +=
modelData.metrics.cache_creation_input_tokens || 0;
// Add daily data
modelMetrics[model].daily_data.push({
@@ -297,17 +596,21 @@ export const processActivityData = (dailyActivity: { results: DailyData[] }, key
spend: modelData.metrics.spend,
successful_requests: modelData.metrics.successful_requests,
failed_requests: modelData.metrics.failed_requests,
cache_read_input_tokens: modelData.metrics.cache_read_input_tokens || 0,
cache_creation_input_tokens: modelData.metrics.cache_creation_input_tokens || 0
}
cache_read_input_tokens:
modelData.metrics.cache_read_input_tokens || 0,
cache_creation_input_tokens:
modelData.metrics.cache_creation_input_tokens || 0,
},
});
});
});
// Sort daily data
Object.values(modelMetrics).forEach(metrics => {
metrics.daily_data.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
Object.values(modelMetrics).forEach((metrics) => {
metrics.daily_data.sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
);
});
return modelMetrics;
};
};
@@ -1,18 +1,38 @@
import React, { useState, useEffect } from "react";
import {
BarChart, Card, Title, Text,
Grid, Col, DateRangePicker, DateRangePickerValue,
Table, TableHead, TableRow, TableHeaderCell, TableBody, TableCell,
import {
BarChart,
Card,
Title,
Text,
Grid,
Col,
DateRangePickerValue,
Table,
TableHead,
TableRow,
TableHeaderCell,
TableBody,
TableCell,
DonutChart,
TabPanel, TabGroup, TabList, Tab, TabPanels
TabPanel,
TabGroup,
TabList,
Tab,
TabPanels,
Subtitle,
} from "@tremor/react";
import UsageDatePicker from "./shared/usage_date_picker";
import { Select } from 'antd';
import { ActivityMetrics, processActivityData } from './activity_metrics';
import { DailyData, KeyMetricWithMetadata, EntityMetricWithMetadata } from './usage/types';
import { tagDailyActivityCall, teamDailyActivityCall } from './networking';
import { Select } from "antd";
import { ActivityMetrics, processActivityData } from "./activity_metrics";
import {
DailyData,
KeyMetricWithMetadata,
EntityMetricWithMetadata,
} from "./usage/types";
import { tagDailyActivityCall, teamDailyActivityCall } from "./networking";
import TopKeyView from "./top_key_view";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { valueFormatterSpend } from "./usage/utils/value_formatters";
interface EntityMetrics {
metrics: {
@@ -58,7 +78,7 @@ export interface EntityList {
interface EntityUsageProps {
accessToken: string | null;
entityType: 'tag' | 'team';
entityType: "tag" | "team";
entityId?: string | null;
userID: string | null;
userRole: string | null;
@@ -73,17 +93,17 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
userID,
userRole,
entityList,
premiumUser
premiumUser,
}) => {
const [spendData, setSpendData] = useState<EntitySpendData>({
results: [],
const [spendData, setSpendData] = useState<EntitySpendData>({
results: [],
metadata: {
total_spend: 0,
total_api_requests: 0,
total_successful_requests: 0,
total_failed_requests: 0,
total_tokens: 0
}
total_tokens: 0,
},
});
const modelMetrics = processActivityData(spendData, "models");
@@ -99,22 +119,22 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
// Create new Date objects to avoid mutating the original dates
const startTime = new Date(dateValue.from);
const endTime = new Date(dateValue.to);
if (entityType === 'tag') {
if (entityType === "tag") {
const data = await tagDailyActivityCall(
accessToken,
startTime,
endTime,
1,
accessToken,
startTime,
endTime,
1,
selectedTags.length > 0 ? selectedTags : null
);
setSpendData(data);
} else if (entityType === 'team') {
} else if (entityType === "team") {
const data = await teamDailyActivityCall(
accessToken,
startTime,
endTime,
1,
accessToken,
startTime,
endTime,
1,
selectedTags.length > 0 ? selectedTags : null
);
setSpendData(data);
@@ -129,7 +149,7 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
const getTopModels = () => {
const modelSpend: { [key: string]: any } = {};
spendData.results.forEach(day => {
spendData.results.forEach((day) => {
Object.entries(day.breakdown.models || {}).forEach(([model, metrics]) => {
if (!modelSpend[model]) {
modelSpend[model] = {
@@ -137,25 +157,28 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
requests: 0,
successful_requests: 0,
failed_requests: 0,
tokens: 0
tokens: 0,
};
}
try {
modelSpend[model].spend += metrics.metrics.spend;
} catch (e) {
console.log(`Error adding spend for ${model}: ${e}, got metrics: ${JSON.stringify(metrics)}`);
console.error(
`Error adding spend for ${model}: ${e}, got metrics: ${JSON.stringify(metrics)}`
);
}
modelSpend[model].requests += metrics.metrics.api_requests;
modelSpend[model].successful_requests += metrics.metrics.successful_requests;
modelSpend[model].successful_requests +=
metrics.metrics.successful_requests;
modelSpend[model].failed_requests += metrics.metrics.failed_requests;
modelSpend[model].tokens += metrics.metrics.total_tokens;
});
});
return Object.entries(modelSpend)
.map(([model, metrics]) => ({
key: model,
...metrics
...metrics,
}))
.sort((a, b) => b.spend - a.spend)
.slice(0, 5);
@@ -163,7 +186,7 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
const getTopAPIKeys = () => {
const keySpend: { [key: string]: KeyMetricWithMetadata } = {};
spendData.results.forEach(day => {
spendData.results.forEach((day) => {
Object.entries(day.breakdown.api_keys || {}).forEach(([key, metrics]) => {
if (!keySpend[key]) {
keySpend[key] = {
@@ -176,25 +199,30 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
successful_requests: 0,
failed_requests: 0,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0
cache_creation_input_tokens: 0,
},
metadata: {
key_alias: metrics.metadata.key_alias
}
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.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;
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,
@@ -207,32 +235,36 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
const getProviderSpend = () => {
const providerSpend: { [key: string]: any } = {};
spendData.results.forEach(day => {
Object.entries(day.breakdown.providers || {}).forEach(([provider, metrics]) => {
if (!providerSpend[provider]) {
providerSpend[provider] = {
provider,
spend: 0,
requests: 0,
successful_requests: 0,
failed_requests: 0,
tokens: 0
};
spendData.results.forEach((day) => {
Object.entries(day.breakdown.providers || {}).forEach(
([provider, metrics]) => {
if (!providerSpend[provider]) {
providerSpend[provider] = {
provider,
spend: 0,
requests: 0,
successful_requests: 0,
failed_requests: 0,
tokens: 0,
};
}
try {
providerSpend[provider].spend += metrics.metrics.spend;
providerSpend[provider].requests += metrics.metrics.api_requests;
providerSpend[provider].successful_requests +=
metrics.metrics.successful_requests;
providerSpend[provider].failed_requests +=
metrics.metrics.failed_requests;
providerSpend[provider].tokens += metrics.metrics.total_tokens;
} catch (e) {
console.error(`Error processing provider ${provider}: ${e}`);
}
}
try {
providerSpend[provider].spend += metrics.metrics.spend;
providerSpend[provider].requests += metrics.metrics.api_requests;
providerSpend[provider].successful_requests += metrics.metrics.successful_requests;
providerSpend[provider].failed_requests += metrics.metrics.failed_requests;
providerSpend[provider].tokens += metrics.metrics.total_tokens;
} catch (e) {
console.log(`Error processing provider ${provider}: ${e}`);
}
});
);
});
return Object.values(providerSpend)
.filter(provider => provider.spend > 0)
.filter((provider) => provider.spend > 0)
.sort((a, b) => b.spend - a.spend);
};
@@ -244,12 +276,12 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
const filterDataByTags = (data: EntityMetricWithMetadata[]) => {
if (selectedTags.length === 0) return data;
return data.filter(item => selectedTags.includes(item.metadata.id));
return data.filter((item) => selectedTags.includes(item.metadata.id));
};
const getEntityBreakdown = () => {
const entitySpend: { [key: string]: EntityMetricWithMetadata } = {};
spendData.results.forEach(day => {
spendData.results.forEach((day) => {
Object.entries(day.breakdown.entities || {}).forEach(([entity, data]) => {
if (!entitySpend[entity]) {
entitySpend[entity] = {
@@ -262,55 +294,68 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
successful_requests: 0,
failed_requests: 0,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0
cache_creation_input_tokens: 0,
},
metadata: {
alias: data.metadata.team_alias || entity,
id: entity
}
id: entity,
},
};
}
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.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.metrics.spend - a.metrics.spend);
const result = Object.values(entitySpend).sort(
(a, b) => b.metrics.spend - a.metrics.spend
);
return filterDataByTags(result);
};
const getProcessedEntityBreakdownForChart = () => {
const data = getEntityBreakdown();
const topEntities = data.slice(0, 5);
return topEntities.map((e) => ({
...e,
metadata: {
...e.metadata,
alias_display:
e.metadata.alias && e.metadata.alias.length > 15
? `${e.metadata.alias.slice(0, 15)}...`
: e.metadata.alias,
},
}));
};
return (
<div style={{ width: "100%" }}>
<Grid numItems={2} className="gap-2 w-full mb-4">
<Col>
<UsageDatePicker value={dateValue} onValueChange={setDateValue} />
</Col>
{entityList && entityList.length > 0 && (
<Col>
<UsageDatePicker
value={dateValue}
onValueChange={setDateValue}
/>
</Col>
{entityList && entityList.length > 0 && (
<Col>
<Text>Filter by {entityType === 'tag' ? 'Tags' : 'Teams'}</Text>
<Select
<Text>Filter by {entityType === "tag" ? "Tags" : "Teams"}</Text>
<Select
mode="multiple"
style={{ width: '100%' }}
placeholder={`Select ${entityType === 'tag' ? 'tags' : 'teams'} to filter...`}
style={{ width: "100%" }}
placeholder={`Select ${entityType === "tag" ? "tags" : "teams"} to filter...`}
value={selectedTags}
onChange={setSelectedTags}
options={getAllTags()}
className="mt-2"
allowClear
/>
</Col>
)}
</Grid>
/>
</Col>
)}
</Grid>
<TabGroup>
<TabList variant="solid" className="mt-1">
<Tab>Cost</Tab>
@@ -323,12 +368,18 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
{/* Total Spend Card */}
<Col numColSpan={2}>
<Card>
<Title>{entityType === 'tag' ? 'Tag' : 'Team'} Spend Overview</Title>
<Title>
{entityType === "tag" ? "Tag" : "Team"} Spend Overview
</Title>
<Grid numItems={5} className="gap-4 mt-4">
<Card>
<Title>Total Spend</Title>
<Text className="text-2xl font-bold mt-2">
${formatNumberWithCommas(spendData.metadata.total_spend, 2)}
$
{formatNumberWithCommas(
spendData.metadata.total_spend,
2
)}
</Text>
</Card>
<Card>
@@ -361,45 +412,89 @@ 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) => `$${formatNumberWithCommas(value, 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: ${formatNumberWithCommas(data.metrics.spend, 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}: ${formatNumberWithCommas(metrics.metrics.spend, 2)}
</p>
);
})}
</div>
</div>
);
}}
/>
</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={valueFormatterSpend}
yAxisWidth={100}
showLegend={false}
customTooltip={({ payload, active }) => {
if (!active || !payload?.[0]) return null;
const data = payload[0].payload;
const entityCount = Object.keys(
data.breakdown.entities || {}
).length;
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: $
{formatNumberWithCommas(data.metrics.spend, 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>
<p className="text-gray-600">
{entityType === "tag"
? "Total Tags"
: "Total Teams"}
: {entityCount}
</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 || {})
.sort(([, a], [, b]) => {
const spendA = (a as EntityMetrics).metrics
.spend;
const spendB = (b as EntityMetrics).metrics
.spend;
return spendB - spendA;
})
.slice(0, 5)
.map(([entity, entityData]) => {
const metrics = entityData as EntityMetrics;
return (
<p
key={entity}
className="text-sm text-gray-600"
>
{metrics.metadata.team_alias || entity}: $
{formatNumberWithCommas(
metrics.metrics.spend,
2
)}
</p>
);
})}
{entityCount > 5 && (
<p className="text-sm text-gray-500 italic">
...and {entityCount - 5} more
</p>
)}
</div>
</div>
);
}}
/>
</Card>
</Col>
{/* Entity Breakdown Section */}
@@ -407,76 +502,137 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
<Card>
<div className="flex flex-col space-y-4">
<div className="flex flex-col space-y-2">
<Title>Spend Per {entityType === 'tag' ? 'Tag' : 'Team'}</Title>
<Title>
Spend Per {entityType === "tag" ? "Tag" : "Team"}
</Title>
<Subtitle className="text-xs">
Showing Top 5 by Spend
</Subtitle>
<div className="flex items-center text-sm text-gray-500">
<span>Get Started Tracking cost per {entityType} </span>
<a href="https://docs.litellm.ai/docs/proxy/enterprise#spend-tracking" className="text-blue-500 hover:text-blue-700 ml-1">
<span>
Get Started by Tracking cost per {entityType}{" "}
</span>
<a
href="https://docs.litellm.ai/docs/proxy/enterprise#spend-tracking"
className="text-blue-500 hover:text-blue-700 ml-1"
>
here
</a>
</div>
</div>
<Grid numItems={2}>
<Col numColSpan={1}>
<Grid numItems={2} className="gap-6">
<Col numColSpan={1}>
<BarChart
className="mt-4 h-52"
data={getEntityBreakdown()}
index="metadata.alias"
categories={["metrics.spend"]}
colors={["cyan"]}
valueFormatter={(value) => `$${formatNumberWithCommas(value, 4)}`}
valueFormatter={valueFormatterSpend}
layout="vertical"
showLegend={false}
yAxisWidth={100}
yAxisWidth={150}
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.metadata.alias}
</p>
<p className="text-cyan-500">
Spend: $
{formatNumberWithCommas(
data.metrics.spend,
4
)}
</p>
<p className="text-gray-600">
Requests:{" "}
{data.metrics.api_requests.toLocaleString()}
</p>
<p className="text-green-600">
Successful:{" "}
{data.metrics.successful_requests.toLocaleString()}
</p>
<p className="text-red-600">
Failed:{" "}
{data.metrics.failed_requests.toLocaleString()}
</p>
<p className="text-gray-600">
Tokens:{" "}
{data.metrics.total_tokens.toLocaleString()}
</p>
</div>
);
}}
/>
</Col>
<Col numColSpan={1}>
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>{entityType === 'tag' ? 'Tag' : 'Team'}</TableHeaderCell>
<TableHeaderCell>Spend</TableHeaderCell>
<TableHeaderCell className="text-green-600">Successful</TableHeaderCell>
<TableHeaderCell className="text-red-600">Failed</TableHeaderCell>
<TableHeaderCell>Tokens</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{getEntityBreakdown()
.filter(entity => entity.metrics.spend > 0)
.map((entity) => (
<TableRow key={entity.metadata.id}>
<TableCell>{entity.metadata.alias}</TableCell>
<TableCell>${formatNumberWithCommas(entity.metrics.spend, 4)}</TableCell>
<TableCell className="text-green-600">
{entity.metrics.successful_requests.toLocaleString()}
</TableCell>
<TableCell className="text-red-600">
{entity.metrics.failed_requests.toLocaleString()}
</TableCell>
<TableCell>{entity.metrics.total_tokens.toLocaleString()}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<div className="h-52 overflow-y-auto">
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>
{entityType === "tag" ? "Tag" : "Team"}
</TableHeaderCell>
<TableHeaderCell>Spend</TableHeaderCell>
<TableHeaderCell className="text-green-600">
Successful
</TableHeaderCell>
<TableHeaderCell className="text-red-600">
Failed
</TableHeaderCell>
<TableHeaderCell>Tokens</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{getEntityBreakdown()
.filter((entity) => entity.metrics.spend > 0)
.map((entity) => (
<TableRow key={entity.metadata.id}>
<TableCell>
{entity.metadata.alias}
</TableCell>
<TableCell>
$
{formatNumberWithCommas(
entity.metrics.spend,
4
)}
</TableCell>
<TableCell className="text-green-600">
{entity.metrics.successful_requests.toLocaleString()}
</TableCell>
<TableCell className="text-red-600">
{entity.metrics.failed_requests.toLocaleString()}
</TableCell>
<TableCell>
{entity.metrics.total_tokens.toLocaleString()}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</Col>
</Grid>
</div>
</Card>
</Col>
{/* Top API Keys */}
<Col numColSpan={1}>
<Card>
<Title>Top API Keys</Title>
<TopKeyView
topKeys={getTopAPIKeys()}
accessToken={accessToken}
userID={userID}
userRole={userRole}
teams={null}
premiumUser={premiumUser}
/>
<TopKeyView
topKeys={getTopAPIKeys()}
accessToken={accessToken}
userID={userID}
userRole={userRole}
teams={null}
premiumUser={premiumUser}
/>
</Card>
</Col>
@@ -490,7 +646,9 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
index="key"
categories={["spend"]}
colors={["cyan"]}
valueFormatter={(value) => `$${formatNumberWithCommas(value, 2)}`}
valueFormatter={(value) =>
`$${formatNumberWithCommas(value, 2)}`
}
layout="vertical"
yAxisWidth={200}
showLegend={false}
@@ -498,8 +656,6 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
</Card>
</Col>
{/* Spend by Provider */}
<Col numColSpan={2}>
<Card>
@@ -512,8 +668,16 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
data={getProviderSpend()}
index="provider"
category="spend"
valueFormatter={(value) => `$${formatNumberWithCommas(value, 2)}`}
colors={["cyan", "blue", "indigo", "violet", "purple"]}
valueFormatter={(value) =>
`$${formatNumberWithCommas(value, 2)}`
}
colors={[
"cyan",
"blue",
"indigo",
"violet",
"purple",
]}
/>
</Col>
<Col numColSpan={1}>
@@ -522,8 +686,12 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
<TableRow>
<TableHeaderCell>Provider</TableHeaderCell>
<TableHeaderCell>Spend</TableHeaderCell>
<TableHeaderCell className="text-green-600">Successful</TableHeaderCell>
<TableHeaderCell className="text-red-600">Failed</TableHeaderCell>
<TableHeaderCell className="text-green-600">
Successful
</TableHeaderCell>
<TableHeaderCell className="text-red-600">
Failed
</TableHeaderCell>
<TableHeaderCell>Tokens</TableHeaderCell>
</TableRow>
</TableHead>
@@ -531,14 +699,18 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
{getProviderSpend().map((provider) => (
<TableRow key={provider.provider}>
<TableCell>{provider.provider}</TableCell>
<TableCell>${formatNumberWithCommas(provider.spend, 2)}</TableCell>
<TableCell>
${formatNumberWithCommas(provider.spend, 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>
<TableCell>
{provider.tokens.toLocaleString()}
</TableCell>
</TableRow>
))}
</TableBody>
@@ -551,10 +723,10 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
</Grid>
</TabPanel>
<TabPanel>
<ActivityMetrics modelMetrics={modelMetrics} />
<ActivityMetrics modelMetrics={modelMetrics} />
</TabPanel>
<TabPanel>
<ActivityMetrics modelMetrics={keyMetrics} />
<ActivityMetrics modelMetrics={keyMetrics} />
</TabPanel>
</TabPanels>
</TabGroup>
@@ -562,4 +734,4 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
);
};
export default EntityUsage;
export default EntityUsage;
+288 -154
View File
@@ -1,19 +1,34 @@
/**
* 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
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 UsageDatePicker from "./shared/usage_date_picker";
import { AreaChart } from "@tremor/react";
@@ -22,13 +37,27 @@ 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 { 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";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { valueFormatterSpend } from "./usage/utils/value_formatters";
interface NewUsagePageProps {
accessToken: string | null;
@@ -43,7 +72,7 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
userRole,
userID,
teams,
premiumUser
premiumUser,
}) => {
const [userSpendData, setUserSpendData] = useState<{
results: DailyData[];
@@ -56,17 +85,19 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
to: new Date(),
});
const [allTags, setAllTags] = useState<EntityList[]>([]);
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
})));
setAllTags(
Object.values(tags).map((tag: Tag) => ({
label: tag.name,
value: tag.name,
}))
);
};
useEffect(() => {
@@ -79,7 +110,7 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
// Calculate top models from the breakdown data
const getTopModels = () => {
const modelSpend: { [key: string]: MetricWithMetadata } = {};
userSpendData.results.forEach(day => {
userSpendData.results.forEach((day) => {
Object.entries(day.breakdown.models || {}).forEach(([model, metrics]) => {
if (!modelSpend[model]) {
modelSpend[model] = {
@@ -92,23 +123,29 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
successful_requests: 0,
failed_requests: 0,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0
cache_creation_input_tokens: 0,
},
metadata: {}
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.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;
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,
@@ -116,7 +153,7 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
requests: metrics.metrics.api_requests,
successful_requests: metrics.metrics.successful_requests,
failed_requests: metrics.metrics.failed_requests,
tokens: metrics.metrics.total_tokens
tokens: metrics.metrics.total_tokens,
}))
.sort((a, b) => b.spend - a.spend)
.slice(0, 5);
@@ -125,51 +162,60 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
// 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: {}
};
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;
}
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
}));
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 => {
userSpendData.results.forEach((day) => {
Object.entries(day.breakdown.api_keys || {}).forEach(([key, metrics]) => {
if (!keySpend[key]) {
keySpend[key] = {
@@ -182,25 +228,30 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
successful_requests: 0,
failed_requests: 0,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0
cache_creation_input_tokens: 0,
},
metadata: {
key_alias: metrics.metadata.key_alias
}
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.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;
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,
@@ -216,14 +267,20 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
// Create new Date objects to avoid mutating the original dates
const startTime = new Date(dateValue.from);
const endTime = new Date(dateValue.to);
try {
// Get first page
const firstPageData = await userDailyActivityCall(accessToken, startTime, endTime);
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.");
throw new Error(
"Too many pages of data (>10). Please select a smaller date range."
);
}
// If only one page, just set the data
@@ -235,23 +292,32 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
// Fetch all pages
const allResults = [...firstPageData.results];
const aggregatedMetadata = { ...firstPageData.metadata };
for (let page = 2; page <= firstPageData.metadata.total_pages; page++) {
const pageData = await userDailyActivityCall(accessToken, startTime, endTime, page);
const pageData = await userDailyActivityCall(
accessToken,
startTime,
endTime,
page
);
allResults.push(...pageData.results);
if (pageData.metadata) {
aggregatedMetadata.total_spend += pageData.metadata.total_spend || 0;
aggregatedMetadata.total_api_requests += pageData.metadata.total_api_requests || 0;
aggregatedMetadata.total_successful_requests += pageData.metadata.total_successful_requests || 0;
aggregatedMetadata.total_failed_requests += pageData.metadata.total_failed_requests || 0;
aggregatedMetadata.total_tokens += pageData.metadata.total_tokens || 0;
aggregatedMetadata.total_spend += pageData.metadata.total_spend || 0;
aggregatedMetadata.total_api_requests +=
pageData.metadata.total_api_requests || 0;
aggregatedMetadata.total_successful_requests +=
pageData.metadata.total_successful_requests || 0;
aggregatedMetadata.total_failed_requests +=
pageData.metadata.total_failed_requests || 0;
aggregatedMetadata.total_tokens +=
pageData.metadata.total_tokens || 0;
}
}
// Combine all results with the first page's metadata
setUserSpendData({
results: allResults,
metadata: aggregatedMetadata
metadata: aggregatedMetadata,
});
} catch (error) {
console.error("Error fetching user spend data:", error);
@@ -268,16 +334,33 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
return (
<div style={{ width: "100%" }} className="p-8">
{all_admin_roles.includes(userRole || "") ?
<Text className="text-sm text-gray-500 mb-4">
Note: If you see key/model-level inconsistencies between Global View and Team Usage, it&apos;s because the Global View was missing spend when user_id = null, prior to v1.71.2. <a href="https://github.com/BerriAI/litellm/issues/10876" className="text-blue-500 hover:text-blue-700 ml-1">Learn more here</a>.
</Text>
: null}
{all_admin_roles.includes(userRole || "") ? (
<Text className="text-sm text-gray-500 mb-4">
Note: If you see key/model-level inconsistencies between Global View
and Team Usage, it&apos;s because the Global View was missing spend
when user_id = null, prior to v1.71.2.{" "}
<a
href="https://github.com/BerriAI/litellm/issues/10876"
className="text-blue-500 hover:text-blue-700 ml-1"
>
Learn more here
</a>
.
</Text>
) : null}
<TabGroup>
<TabList variant="solid" className="mt-1">
{all_admin_roles.includes(userRole || "") ? <Tab>Global Usage</Tab> : <Tab>Your Usage</Tab>}
{all_admin_roles.includes(userRole || "") ? (
<Tab>Global Usage</Tab>
) : (
<Tab>Your Usage</Tab>
)}
<Tab>Team Usage</Tab>
{all_admin_roles.includes(userRole || "") ? <Tab>Tag Usage</Tab> : <></>}
{all_admin_roles.includes(userRole || "") ? (
<Tab>Tag Usage</Tab>
) : (
<></>
)}
</TabList>
<TabPanels>
{/* Your Usage Panel */}
@@ -305,9 +388,18 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
{/* 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()}
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}
@@ -325,31 +417,41 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
<Card>
<Title>Total Requests</Title>
<Text className="text-2xl font-bold mt-2">
{userSpendData.metadata?.total_api_requests?.toLocaleString() || 0}
{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}
{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}
{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}
{userSpendData.metadata?.total_tokens?.toLocaleString() ||
0}
</Text>
</Card>
<Card>
<Title>Average Cost per Request</Title>
<Text className="text-2xl font-bold mt-2">
${formatNumberWithCommas(((totalSpend || 0) / (userSpendData.metadata?.total_api_requests || 1)), 4)}
$
{formatNumberWithCommas(
(totalSpend || 0) /
(userSpendData.metadata?.total_api_requests ||
1),
4
)}
</Text>
</Card>
</Grid>
@@ -361,13 +463,15 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
<Card>
<Title>Daily Spend</Title>
<BarChart
data={[...userSpendData.results].sort((a, b) =>
new Date(a.date).getTime() - new Date(b.date).getTime()
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) => `$${formatNumberWithCommas(value, 2)}`}
valueFormatter={valueFormatterSpend}
yAxisWidth={100}
showLegend={false}
customTooltip={({ payload, active }) => {
@@ -376,11 +480,25 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
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: ${formatNumberWithCommas(data.metrics.spend, 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>
<p className="text-cyan-500">
Spend: $
{formatNumberWithCommas(
data.metrics.spend,
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>
);
}}
@@ -414,7 +532,7 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
index="key"
categories={["spend"]}
colors={["cyan"]}
valueFormatter={(value) => `$${formatNumberWithCommas(value, 2)}`}
valueFormatter={valueFormatterSpend}
layout="vertical"
yAxisWidth={200}
showLegend={false}
@@ -424,11 +542,25 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
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: ${formatNumberWithCommas(data.spend, 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>
<p className="text-cyan-500">
Spend: $
{formatNumberWithCommas(data.spend, 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>
);
}}
@@ -449,7 +581,9 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
data={getProviderSpend()}
index="provider"
category="spend"
valueFormatter={(value) => `$${formatNumberWithCommas(value, 2)}`}
valueFormatter={(value) =>
`$${formatNumberWithCommas(value, 2)}`
}
colors={["cyan"]}
/>
</Col>
@@ -459,29 +593,39 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
<TableRow>
<TableHeaderCell>Provider</TableHeaderCell>
<TableHeaderCell>Spend</TableHeaderCell>
<TableHeaderCell className="text-green-600">Successful</TableHeaderCell>
<TableHeaderCell className="text-red-600">Failed</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)
.filter((provider) => provider.spend > 0)
.map((provider) => (
<TableRow key={provider.provider}>
<TableCell>{provider.provider}</TableCell>
<TableCell>
${formatNumberWithCommas(provider.spend, 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>
))}
$
{formatNumberWithCommas(
provider.spend,
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>
@@ -490,7 +634,6 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
</Col>
{/* Usage Metrics */}
</Grid>
</TabPanel>
@@ -507,22 +650,24 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
{/* Team Usage Panel */}
<TabPanel>
<EntityUsage
<EntityUsage
accessToken={accessToken}
entityType="team"
userID={userID}
userRole={userRole}
entityList={teams?.map(team => ({
label: team.team_alias,
value: team.team_id
})) || null}
entityList={
teams?.map((team) => ({
label: team.team_alias,
value: team.team_id,
})) || null
}
premiumUser={premiumUser}
/>
</TabPanel>
{/* Tag Usage Panel */}
<TabPanel>
<EntityUsage
<EntityUsage
accessToken={accessToken}
entityType="tag"
userID={userID}
@@ -531,7 +676,6 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
premiumUser={premiumUser}
/>
</TabPanel>
</TabPanels>
</TabGroup>
</div>
@@ -561,16 +705,16 @@ const getModelActivityData = (userSpendData: {
modelData[model] = {
total_requests: 0,
total_tokens: 0,
daily_data: []
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
total_tokens: metrics.metrics.total_tokens,
});
});
});
@@ -578,14 +722,4 @@ const getModelActivityData = (userSpendData: {
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;
export default NewUsagePage;
@@ -994,7 +994,7 @@ const UsagePage: React.FC<UsagePageProps> = ({
<Card>
<Title>Spend Per Tag</Title>
<Text>Get Started Tracking cost per tag <a className="text-blue-500" href="https://docs.litellm.ai/docs/proxy/cost_tracking" target="_blank">here</a></Text>
<Text>Get Started by Tracking cost per tag <a className="text-blue-500" href="https://docs.litellm.ai/docs/proxy/cost_tracking" target="_blank">here</a></Text>
<BarChart
className="h-72"
data={topTagsData}
@@ -0,0 +1,20 @@
export function valueFormatter(number: number) {
if (number >= 1000000) {
return (number / 1000000).toFixed(2) + "M";
}
if (number >= 1000) {
return number / 1000 + "k";
}
return number.toString();
}
export function valueFormatterSpend(number: number) {
if (number === 0) return "$0";
if (number >= 1000000) {
return "$" + number / 1000000 + "M";
}
if (number >= 1000) {
return "$" + number / 1000 + "k";
}
return "$" + number;
}