Model Usage per key

This commit is contained in:
yuneng-jiang
2026-01-29 18:27:10 -08:00
parent f7e1a22947
commit aa75bec100
3 changed files with 182 additions and 18 deletions
@@ -0,0 +1,108 @@
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { BarChart, Card, Title } from "@tremor/react";
import { Table } from "antd";
import type { ColumnsType } from "antd/es/table";
import React, { useState } from "react";
import { TopModelData } from "../types";
interface KeyModelUsageViewProps {
topModels: TopModelData[];
}
const VISIBLE_ROWS = 5;
// antd Table with size="small" has a row height of ~39px
const ANTD_SMALL_TABLE_ROW_HEIGHT = 39;
const columns: ColumnsType<TopModelData> = [
{
title: "Model",
dataIndex: "model",
key: "model",
render: (value) => value || "-",
},
{
title: "Spend (USD)",
dataIndex: "spend",
key: "spend",
render: (value) => `$${formatNumberWithCommas(value, 2)}`,
},
{
title: "Successful",
dataIndex: "successful_requests",
key: "successful_requests",
render: (value) => <span className="text-green-600">{value?.toLocaleString() || 0}</span>,
},
{
title: "Failed",
dataIndex: "failed_requests",
key: "failed_requests",
render: (value) => <span className="text-red-600">{value?.toLocaleString() || 0}</span>,
},
{
title: "Tokens",
dataIndex: "tokens",
key: "tokens",
render: (value) => value?.toLocaleString() || 0,
},
];
const KeyModelUsageView: React.FC<KeyModelUsageViewProps> = ({ topModels }) => {
const [viewMode, setViewMode] = useState<"chart" | "table">("table");
if (topModels.length === 0) {
return null;
}
return (
<Card className="mt-4">
<div className="flex justify-between items-center mb-3">
<Title>Model Usage</Title>
<div className="flex space-x-2">
<button
onClick={() => setViewMode("table")}
className={`px-3 py-1 text-sm rounded-md ${viewMode === "table" ? "bg-blue-100 text-blue-700" : "bg-gray-100 text-gray-700"}`}
>
Table
</button>
<button
onClick={() => setViewMode("chart")}
className={`px-3 py-1 text-sm rounded-md ${viewMode === "chart" ? "bg-blue-100 text-blue-700" : "bg-gray-100 text-gray-700"}`}
>
Chart
</button>
</div>
</div>
{viewMode === "chart" ? (
<div className="max-h-[234px] overflow-y-auto">
<BarChart
style={{ height: topModels.length * 40 }}
data={topModels.map((m) => ({ key: m.model, spend: m.spend }))}
index="key"
categories={["spend"]}
colors={["cyan"]}
valueFormatter={(value) => `$${formatNumberWithCommas(value, 2)}`}
layout="vertical"
yAxisWidth={180}
tickGap={5}
showLegend={false}
/>
</div>
) : (
<Table
columns={columns}
dataSource={topModels}
rowKey="model"
size="small"
pagination={false}
scroll={
topModels.length > VISIBLE_ROWS
? { y: VISIBLE_ROWS * ANTD_SMALL_TABLE_ROW_HEIGHT }
: undefined
}
/>
)}
</Card>
);
};
export default KeyModelUsageView;
@@ -52,6 +52,15 @@ export interface TopApiKeyData {
tokens: number;
}
export interface TopModelData {
model: string;
spend: number;
requests: number;
successful_requests: number;
failed_requests: number;
tokens: number;
}
export interface ModelActivityData {
label: string;
total_requests: number;
@@ -64,6 +73,7 @@ export interface ModelActivityData {
completion_tokens: number;
total_spend: number;
top_api_keys: TopApiKeyData[];
top_models: TopModelData[];
daily_data: {
date: string;
metrics: {
@@ -5,7 +5,8 @@ import { Collapse } from "antd";
import React from "react";
import { CustomLegend, CustomTooltip } from "./common_components/chartUtils";
import { Team } from "./key_team_helpers/key_list";
import { DailyData, KeyMetricWithMetadata, ModelActivityData, TopApiKeyData } from "./UsagePage/types";
import KeyModelUsageView from "./UsagePage/components/KeyModelUsageView";
import { DailyData, KeyMetricWithMetadata, ModelActivityData, TopApiKeyData, TopModelData } from "./UsagePage/types";
import { valueFormatter } from "./UsagePage/utils/value_formatters";
interface ActivityMetricsProps {
@@ -72,8 +73,29 @@ const ModelSection = ({
</Card>
)}
{metrics.top_models && metrics.top_models.length > 0 && (
<KeyModelUsageView topModels={metrics.top_models} />
)}
{/* Spend per day - Full width card */}
<Card className="mt-4">
<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, true)}`}
yAxisWidth={72}
/>
</Card>
{/* Charts */}
<Grid numItems={2} className="gap-4">
<Grid numItems={2} className="gap-4 mt-4">
<Card>
<div className="flex justify-between items-center">
<Title>Total Tokens</Title>
@@ -111,22 +133,6 @@ const ModelSection = ({
/>
</Card>
<Card>
<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, true)}`}
yAxisWidth={72}
/>
</Card>
<Card>
<div className="flex justify-between items-center">
<Title>Success vs Failed Requests</Title>
@@ -377,6 +383,7 @@ export const processActivityData = (
total_cache_read_input_tokens: 0,
total_cache_creation_input_tokens: 0,
top_api_keys: [],
top_models: [],
daily_data: [],
};
}
@@ -444,6 +451,45 @@ export const processActivityData = (
});
}
// Process Model breakdowns for each API key (only when key is 'api_keys')
if (key === "api_keys") {
Object.entries(modelMetrics).forEach(([apiKeyHash, _]) => {
const modelBreakdown: Record<string, TopModelData> = {};
// Aggregate Model data for this key across all days
// We need to look in breakdown.models[model].api_key_breakdown[apiKeyHash]
dailyActivity.results.forEach((day) => {
Object.entries(day.breakdown.models || {}).forEach(([modelName, modelData]) => {
if (modelData && "api_key_breakdown" in modelData) {
const keyDataForModel = modelData.api_key_breakdown?.[apiKeyHash];
if (keyDataForModel) {
if (!modelBreakdown[modelName]) {
modelBreakdown[modelName] = {
model: modelName,
spend: 0,
requests: 0,
successful_requests: 0,
failed_requests: 0,
tokens: 0,
};
}
modelBreakdown[modelName].spend += keyDataForModel.metrics.spend;
modelBreakdown[modelName].requests += keyDataForModel.metrics.api_requests;
modelBreakdown[modelName].successful_requests += keyDataForModel.metrics.successful_requests || 0;
modelBreakdown[modelName].failed_requests += keyDataForModel.metrics.failed_requests || 0;
modelBreakdown[modelName].tokens += keyDataForModel.metrics.total_tokens;
}
}
});
});
// Sort by spend
modelMetrics[apiKeyHash].top_models = Object.values(modelBreakdown)
.sort((a, b) => b.spend - a.spend);
});
}
// Sort daily data
Object.values(modelMetrics).forEach((metrics) => {
metrics.daily_data.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());