mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-11 00:25:15 +00:00
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:
@@ -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;
|
||||
|
||||
@@ -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'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'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;
|
||||
}
|
||||
Reference in New Issue
Block a user