Remove model usage analytics from UI update branch

This commit is contained in:
Evan Yang
2026-03-09 15:26:07 +08:00
parent 20b4c3a641
commit 8dbf036558
5 changed files with 0 additions and 1312 deletions
-123
View File
@@ -708,18 +708,6 @@ class LogManager:
"failed": 0,
}
)
model_mix_buckets: dict[str, dict[str, int]] = defaultdict(
lambda: defaultdict(int)
)
model_mix_revenue_buckets: dict[str, dict[str, float]] = defaultdict(
lambda: defaultdict(float)
)
model_mix_token_buckets: dict[str, dict[str, int]] = defaultdict(
lambda: defaultdict(int)
)
model_mix_totals: dict[str, int] = defaultdict(int)
model_mix_revenue_totals: dict[str, float] = defaultdict(float)
model_mix_token_totals: dict[str, int] = defaultdict(int)
latest_errors_heap: list[tuple[str, dict[str, Any]]] = []
total_error_count = 0
@@ -777,22 +765,12 @@ class LogManager:
summary_stats["total_tokens"] += input_tokens + output_tokens
model_stats[model]["requests"] += 1
model_stats[model]["successful"] += 1
model_mix_totals[model] += 1
if bucket:
bucket["total_requests"] += 1
bucket["successful_chat_completions"] += 1
bucket["input_tokens"] += input_tokens
bucket["output_tokens"] += output_tokens
bucket["total_tokens"] += input_tokens + output_tokens
if bucket_key:
model_mix_buckets[bucket_key][model] += 1
if revenue_msats > 0:
model_mix_revenue_buckets[bucket_key][model] += revenue_msats
model_mix_revenue_totals[model] += revenue_msats
if input_tokens > 0 or output_tokens > 0:
token_total = input_tokens + output_tokens
model_mix_token_buckets[bucket_key][model] += token_total
model_mix_token_totals[model] += token_total
if revenue_msats > 0:
summary_stats["revenue_msats"] += revenue_msats
@@ -876,95 +854,6 @@ class LogManager:
latest_errors_heap, key=lambda x: x[0], reverse=True
)
]
top_model_limit = max(1, min(model_limit, 20))
top_models_requests = [
model_name
for model_name, _ in sorted(
(
(name, count)
for name, count in model_mix_totals.items()
if name != "unknown"
),
key=lambda item: item[1],
reverse=True,
)[:top_model_limit]
]
top_models_revenue = [
model_name
for model_name, _ in sorted(
(
(name, amount)
for name, amount in model_mix_revenue_totals.items()
if name != "unknown"
),
key=lambda item: item[1],
reverse=True,
)[:top_model_limit]
]
top_models_tokens = [
model_name
for model_name, _ in sorted(
(
(name, token_count)
for name, token_count in model_mix_token_totals.items()
if name != "unknown"
),
key=lambda item: item[1],
reverse=True,
)[:top_model_limit]
]
selected_models: list[str] = []
for model in top_models_requests + top_models_revenue + top_models_tokens:
if model not in selected_models:
selected_models.append(model)
top_model_set = set(selected_models)
model_usage_mix_metrics: list[dict[str, Any]] = []
mix_bucket_keys = sorted(
set(model_mix_buckets.keys())
| set(model_mix_revenue_buckets.keys())
| set(model_mix_token_buckets.keys())
)
for bucket_key in mix_bucket_keys:
counts = model_mix_buckets.get(bucket_key, {})
revenue_counts = model_mix_revenue_buckets.get(bucket_key, {})
token_counts = model_mix_token_buckets.get(bucket_key, {})
others = 0
others_revenue_msats = 0.0
others_tokens = 0
model_counts: dict[str, int] = {}
model_revenue_msats: dict[str, float] = {}
model_tokens: dict[str, int] = {}
for model_name, successful_count in counts.items():
if model_name in top_model_set:
model_counts[model_name] = int(successful_count)
else:
others += int(successful_count)
for model_name, revenue_value in revenue_counts.items():
if model_name in top_model_set:
model_revenue_msats[model_name] = float(revenue_value)
else:
others_revenue_msats += float(revenue_value)
for model_name, token_value in token_counts.items():
if model_name in top_model_set:
model_tokens[model_name] = int(token_value)
else:
others_tokens += int(token_value)
model_usage_mix_metrics.append(
{
"timestamp": bucket_key,
"total_successful": int(sum(counts.values())),
"total_revenue_msats": float(sum(revenue_counts.values())),
"total_tokens": int(sum(token_counts.values())),
"others": others,
"others_revenue_msats": others_revenue_msats,
"others_tokens": others_tokens,
"model_counts": model_counts,
"model_revenue_msats": model_revenue_msats,
"model_tokens": model_tokens,
}
)
return {
"metrics": {
@@ -983,18 +872,6 @@ class LogManager:
"total_revenue_sats": total_revenue,
"total_models": len(models),
},
"model_usage_mix": {
"top_models": top_models_requests,
"top_models_by_metric": {
"requests": top_models_requests,
"revenue": top_models_revenue,
"tokens": top_models_tokens,
},
"metrics": model_usage_mix_metrics,
"interval_minutes": interval_minutes,
"hours_back": hours_back,
"total_buckets": len(model_usage_mix_metrics),
},
}
def _aggregate_metrics_by_time(
-201
View File
@@ -60,20 +60,12 @@ class UsageAnalyticsStore:
cutoff_timestamp=cutoff_timestamp,
limit=model_limit,
)
model_usage_mix = self._query_model_usage_mix_locked(
conn,
cutoff_timestamp=cutoff_timestamp,
interval_minutes=interval_minutes,
hours_back=hours_back,
limit=model_limit,
)
return {
"metrics": metrics,
"summary": summary,
"error_details": error_details,
"revenue_by_model": revenue_by_model,
"model_usage_mix": model_usage_mix,
}
def get_summary(self, *, hours_back: int) -> dict[str, Any]:
@@ -1106,199 +1098,6 @@ class UsageAnalyticsStore:
"total_models": len(models),
}
def _query_model_usage_mix_locked(
self,
conn: sqlite3.Connection,
*,
cutoff_timestamp: str,
interval_minutes: int,
hours_back: int,
limit: int,
) -> dict[str, Any]:
top_limit = max(1, min(int(limit), 20))
top_rows_requests = conn.execute(
"""
SELECT
model,
COALESCE(SUM(successful), 0) AS total_successful
FROM analytics_model_minute
WHERE minute_ts >= ?
AND model != 'unknown'
GROUP BY model
ORDER BY total_successful DESC
LIMIT ?
""",
(cutoff_timestamp, top_limit),
).fetchall()
top_rows_revenue = conn.execute(
"""
SELECT
model,
COALESCE(SUM(revenue_msats), 0) AS total_revenue_msats
FROM analytics_model_minute
WHERE minute_ts >= ?
AND model != 'unknown'
GROUP BY model
ORDER BY total_revenue_msats DESC
LIMIT ?
""",
(cutoff_timestamp, top_limit),
).fetchall()
top_rows_tokens = conn.execute(
"""
SELECT
model,
COALESCE(SUM(total_tokens), 0) AS total_tokens
FROM analytics_model_minute
WHERE minute_ts >= ?
AND model != 'unknown'
GROUP BY model
ORDER BY total_tokens DESC
LIMIT ?
""",
(cutoff_timestamp, top_limit),
).fetchall()
top_models_requests = [
str(row["model"])
for row in top_rows_requests
if int(row["total_successful"] or 0) > 0
]
top_models_revenue = [
str(row["model"])
for row in top_rows_revenue
if float(row["total_revenue_msats"] or 0.0) > 0
]
top_models_tokens = [
str(row["model"])
for row in top_rows_tokens
if int(row["total_tokens"] or 0) > 0
]
selected_models: list[str] = []
for model in top_models_requests + top_models_revenue + top_models_tokens:
if model not in selected_models:
selected_models.append(model)
bucket_seconds = max(60, int(interval_minutes) * 60)
total_rows = conn.execute(
"""
SELECT
datetime(
(CAST(strftime('%s', minute_ts) AS INTEGER) / ?) * ?,
'unixepoch'
) AS bucket_ts,
COALESCE(SUM(successful), 0) AS total_successful,
COALESCE(SUM(revenue_msats), 0) AS total_revenue_msats,
COALESCE(SUM(total_tokens), 0) AS total_tokens
FROM analytics_model_minute
WHERE minute_ts >= ?
GROUP BY bucket_ts
ORDER BY bucket_ts
""",
(bucket_seconds, bucket_seconds, cutoff_timestamp),
).fetchall()
bucket_index: dict[str, dict[str, Any]] = {}
for row in total_rows:
total_successful = int(row["total_successful"])
total_revenue_msats = float(row["total_revenue_msats"])
total_tokens = int(row["total_tokens"])
if (
total_successful <= 0
and total_revenue_msats <= 0
and total_tokens <= 0
):
continue
bucket_ts = str(row["bucket_ts"])
bucket = bucket_index.setdefault(
bucket_ts,
{
"timestamp": bucket_ts,
"total_successful": 0,
"total_revenue_msats": 0.0,
"total_tokens": 0,
"others": 0,
"others_revenue_msats": 0.0,
"others_tokens": 0,
"model_counts": {},
"model_revenue_msats": {},
"model_tokens": {},
},
)
bucket["total_successful"] = total_successful
bucket["total_revenue_msats"] = total_revenue_msats
bucket["total_tokens"] = total_tokens
bucket["others"] = total_successful
bucket["others_revenue_msats"] = total_revenue_msats
bucket["others_tokens"] = total_tokens
if selected_models and bucket_index:
placeholders = ",".join("?" for _ in selected_models)
top_model_rows = conn.execute(
f"""
SELECT
datetime(
(CAST(strftime('%s', minute_ts) AS INTEGER) / ?) * ?,
'unixepoch'
) AS bucket_ts,
model,
COALESCE(SUM(successful), 0) AS successful,
COALESCE(SUM(revenue_msats), 0) AS revenue_msats,
COALESCE(SUM(total_tokens), 0) AS total_tokens
FROM analytics_model_minute
WHERE minute_ts >= ?
AND model IN ({placeholders})
GROUP BY bucket_ts, model
ORDER BY bucket_ts
""",
(bucket_seconds, bucket_seconds, cutoff_timestamp, *selected_models),
).fetchall()
for row in top_model_rows:
bucket_ts = str(row["bucket_ts"])
if bucket_ts not in bucket_index:
continue
bucket = bucket_index[bucket_ts]
model = str(row["model"])
successful = int(row["successful"])
revenue_msats = float(row["revenue_msats"])
total_tokens = int(row["total_tokens"])
model_counts = bucket["model_counts"]
model_counts[model] = successful
model_revenue_msats = bucket["model_revenue_msats"]
model_revenue_msats[model] = revenue_msats
model_tokens = bucket["model_tokens"]
model_tokens[model] = total_tokens
bucket["others"] = max(0, int(bucket["others"]) - successful)
bucket["others_revenue_msats"] = max(
0.0,
float(bucket["others_revenue_msats"]) - revenue_msats,
)
bucket["others_tokens"] = max(
0,
int(bucket["others_tokens"]) - total_tokens,
)
metrics = sorted(bucket_index.values(), key=lambda item: str(item["timestamp"]))
return {
"top_models": top_models_requests,
"top_models_by_metric": {
"requests": top_models_requests,
"revenue": top_models_revenue,
"tokens": top_models_tokens,
},
"metrics": metrics,
"hours_back": hours_back,
"interval_minutes": interval_minutes,
"total_buckets": len(metrics),
}
def _cutoff_timestamp(self, hours_back: int) -> str:
cutoff = datetime.now(timezone.utc) - timedelta(hours=hours_back)
return cutoff.strftime("%Y-%m-%d %H:%M:%S")
-14
View File
@@ -9,7 +9,6 @@ import type { DateRange } from 'react-day-picker';
import { UsageMetricsChart } from '@/components/usage-metrics-chart';
import { UsageSummaryCards } from '@/components/usage-summary-cards';
import { ErrorDetailsTable } from '@/components/error-details-table';
import { TopModelsUsageChart } from '@/components/top-models-usage-chart';
import { DashboardBalanceSummary } from '@/components/dashboard-balance-summary';
import {
AdminService,
@@ -589,11 +588,6 @@ export default function DashboardPage() {
const metricsData = usageDashboardData?.metrics;
const summaryData = usageDashboardData?.summary;
const errorData = usageDashboardData?.error_details;
const modelUsageMixData = usageDashboardData?.model_usage_mix;
const hasModelUsageMixMetrics =
Array.isArray(modelUsageMixData?.metrics) &&
modelUsageMixData.metrics.length > 0;
const metricsLoading = usageDashboardLoading;
const summaryLoading = usageDashboardLoading;
const errorLoading = usageDashboardLoading;
@@ -1036,14 +1030,6 @@ export default function DashboardPage() {
</Card>
)}
{!metricsLoading && modelUsageMixData && hasModelUsageMixMetrics ? (
<TopModelsUsageChart
mix={modelUsageMixData}
displayUnit={displayUnit}
usdPerSat={usdPerSat}
/>
) : null}
{summaryLoading ? (
<SectionLoading label='summary' />
) : summaryData ? (
-952
View File
@@ -1,952 +0,0 @@
'use client';
import { useEffect, useMemo, useRef, useState } from 'react';
import { ExpandIcon, Minimize2Icon } from 'lucide-react';
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from 'recharts';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import {
ChartConfig,
ChartContainer,
ChartTooltip,
} from '@/components/ui/chart';
import { useIsMobile } from '@/hooks/use-mobile';
import { type ModelUsageMix } from '@/lib/api/services/admin';
import type { DisplayUnit } from '@/lib/types/units';
import { cn } from '@/lib/utils';
interface TopModelsUsageChartProps {
mix: ModelUsageMix;
displayUnit: DisplayUnit;
usdPerSat: number | null;
}
type ChartMode = 'requests' | 'revenue' | 'tokens';
interface TooltipRow {
color: string;
dataKey: string;
label: string;
value: number;
}
type LeaderboardTrend = 'up' | 'down' | 'flat' | 'new';
interface LeaderboardRow {
chartDataKey: string | null;
displayName: string;
model: string;
provider: string;
rank: number;
totalRaw: number;
trend: LeaderboardTrend;
trendPercent: number | null;
}
function parseBucketDate(value: string): Date | null {
const normalized = value.includes('T')
? value
: `${value.replace(' ', 'T')}Z`;
const parsed = new Date(normalized);
if (!Number.isNaN(parsed.getTime())) {
return parsed;
}
const fallback = new Date(value);
return Number.isNaN(fallback.getTime()) ? null : fallback;
}
function hueFromString(input: string): number {
let hash = 0;
for (let i = 0; i < input.length; i += 1) {
hash = (hash << 5) - hash + input.charCodeAt(i);
hash |= 0;
}
return Math.abs(hash) % 360;
}
function getSeriesColor(model: string, index: number): string {
const palette = [
'var(--chart-1)',
'var(--chart-2)',
'var(--chart-3)',
'var(--chart-4)',
'var(--chart-5)',
'#f59e0b',
'#06b6d4',
'#8b5cf6',
'#f97316',
'#34d399',
];
if (index < palette.length) {
return palette[index];
}
const hue = (hueFromString(model) + index * 23) % 360;
return `hsl(${hue} 70% 56%)`;
}
function formatTooltipTimestamp(
label: string,
intervalMinutes: number,
hoursBack: number
): string {
const date = parseBucketDate(label);
if (!date) {
return label;
}
const shouldShowTime = intervalMinutes <= 6 * 60 || hoursBack <= 48;
if (shouldShowTime) {
return date.toLocaleString([], {
month: 'long',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}
return date.toLocaleString([], {
month: 'long',
day: 'numeric',
year: 'numeric',
});
}
function formatAxisTimestamp(
timestamp: string,
hasMultipleDays: boolean,
intervalMinutes: number,
hoursBack: number
): string {
const date = parseBucketDate(timestamp);
if (!date) {
return '';
}
const shouldShowTime = intervalMinutes <= 6 * 60 || hoursBack <= 48;
if (shouldShowTime && hasMultipleDays) {
return date.toLocaleString([], {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}
if (shouldShowTime) {
return date.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
});
}
if (intervalMinutes >= 24 * 60 && hoursBack >= 24 * 180) {
return date.toLocaleDateString([], {
month: 'short',
year: '2-digit',
});
}
if (hasMultipleDays) {
return date.toLocaleDateString([], {
month: 'short',
day: 'numeric',
});
}
return date.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
});
}
function convertRevenueMsats(
amountMsats: number,
displayUnit: DisplayUnit,
usdPerSat: number | null
): number {
if (displayUnit === 'msat') {
return amountMsats;
}
const sats = amountMsats / 1000;
if (displayUnit === 'usd') {
return sats * (usdPerSat ?? 0);
}
return sats;
}
function prettifyProvider(provider: string): string {
const normalized = provider.trim().toLowerCase();
const aliasMap: Record<string, string> = {
'x ai': 'x-ai',
xai: 'x-ai',
'z ai': 'z-ai',
zai: 'z-ai',
open_ai: 'openai',
openai: 'openai',
};
if (aliasMap[normalized]) {
return aliasMap[normalized];
}
return normalized.replace(/[_-]+/g, ' ');
}
function detectProviderFromModel(model: string): string {
const value = model.toLowerCase();
if (value.includes('claude')) return 'anthropic';
if (value.includes('gpt') || value.includes('openai')) return 'openai';
if (value.includes('gemini')) return 'google';
if (
value.includes('grok') ||
value.includes('x-ai') ||
value.includes('xai')
) {
return 'x-ai';
}
if (value.includes('deepseek')) return 'deepseek';
if (value.includes('minimax')) return 'minimax';
if (value.includes('kimi') || value.includes('moonshot')) return 'moonshot';
if (value.includes('mistral')) return 'mistral';
if (value.includes('qwen') || value.includes('alibaba')) return 'alibaba';
if (
value.includes('glm') ||
value.includes('z-ai') ||
value.includes('z ai')
) {
return 'z-ai';
}
return 'unknown';
}
function getModelPresentation(model: string): {
displayName: string;
provider: string;
} {
const trimmed = model.trim();
const slashIndex = trimmed.indexOf('/');
if (slashIndex > 0 && slashIndex < trimmed.length - 1) {
const provider = prettifyProvider(trimmed.slice(0, slashIndex));
const displayName = trimmed.slice(slashIndex + 1);
return { displayName, provider };
}
return {
displayName: trimmed,
provider: detectProviderFromModel(trimmed),
};
}
export function TopModelsUsageChart({
mix,
displayUnit,
usdPerSat,
}: TopModelsUsageChartProps) {
const [mode, setMode] = useState<ChartMode>('requests');
const [hoveredSeriesKey, setHoveredSeriesKey] = useState<string | null>(null);
const [isChartPointerInside, setIsChartPointerInside] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
const [showAllMobileRows, setShowAllMobileRows] = useState(false);
const isMobile = useIsMobile();
const containerRef = useRef<HTMLDivElement>(null);
const compactNumber = useMemo(
() =>
new Intl.NumberFormat('en-US', {
notation: 'compact',
maximumFractionDigits: 2,
}),
[]
);
const mixTopModels = useMemo(
() => (Array.isArray(mix.top_models) ? mix.top_models : []),
[mix.top_models]
);
const mixMetrics = useMemo(
() => (Array.isArray(mix.metrics) ? mix.metrics : []),
[mix.metrics]
);
const chartModels = useMemo(() => mixTopModels.slice(0, 20), [mixTopModels]);
const leaderboardModels = useMemo(
() => mixTopModels.slice(0, 20),
[mixTopModels]
);
const revenueDisplayUnit: DisplayUnit = useMemo(() => {
if (displayUnit === 'usd' && usdPerSat === null) {
return 'sat';
}
return displayUnit;
}, [displayUnit, usdPerSat]);
const revenueUnitLabel =
revenueDisplayUnit === 'usd'
? 'USD'
: revenueDisplayUnit === 'sat'
? 'sats'
: revenueDisplayUnit === 'msat'
? 'msats'
: revenueDisplayUnit;
const series = useMemo(
() =>
chartModels.map((model, index) => ({
requestsKey: `model_req_${index}`,
revenueKey: `model_rev_${index}`,
tokensKey: `model_tok_${index}`,
label: model,
color: getSeriesColor(model, index),
})),
[chartModels]
);
const chartData = useMemo(
() =>
mixMetrics.map((metric) => {
const modelCounts = metric.model_counts ?? {};
const modelRevenue = metric.model_revenue_msats ?? {};
const modelTokens = metric.model_tokens ?? {};
const point: Record<string, number | string> = {
timestamp: metric.timestamp,
total_successful: metric.total_successful,
total_revenue_msats: metric.total_revenue_msats,
total_tokens: metric.total_tokens,
others_requests: metric.others,
others_revenue_msats: metric.others_revenue_msats,
others_tokens: metric.others_tokens,
};
for (const item of series) {
point[item.requestsKey] = modelCounts[item.label] ?? 0;
point[item.revenueKey] = modelRevenue[item.label] ?? 0;
point[item.tokensKey] = modelTokens[item.label] ?? 0;
}
return point;
}),
[mixMetrics, series]
);
const hasMultipleDays = useMemo(() => {
const daySet = new Set(
chartData.map((item) =>
parseBucketDate(String(item.timestamp))?.toDateString()
)
);
return daySet.size > 1;
}, [chartData]);
const chartConfig = useMemo(() => {
const config: ChartConfig = {};
for (const item of series) {
config[item.requestsKey] = {
label: item.label,
color: item.color,
};
config[item.revenueKey] = {
label: item.label,
color: item.color,
};
config[item.tokensKey] = {
label: item.label,
color: item.color,
};
}
config.others_requests = {
label: 'Others',
color: '#6b7280',
};
config.others_revenue_msats = {
label: 'Others',
color: '#6b7280',
};
config.others_tokens = {
label: 'Others',
color: '#6b7280',
};
return config;
}, [series]);
useEffect(() => {
setHoveredSeriesKey(null);
setIsChartPointerInside(false);
}, [mode]);
useEffect(() => {
const handleFullscreenChange = () => {
setIsFullscreen(document.fullscreenElement === containerRef.current);
};
document.addEventListener('fullscreenchange', handleFullscreenChange);
return () => {
document.removeEventListener('fullscreenchange', handleFullscreenChange);
};
}, []);
const toggleFullscreen = async () => {
if (!containerRef.current) {
return;
}
try {
if (document.fullscreenElement === containerRef.current) {
await document.exitFullscreen();
} else {
await containerRef.current.requestFullscreen();
}
} catch (error) {
console.error('Failed to toggle top models chart fullscreen', error);
}
};
const formatValue = (rawValue: number): string => {
if (mode === 'requests') {
return compactNumber.format(rawValue);
}
if (mode === 'tokens') {
return compactNumber.format(rawValue);
}
const converted = convertRevenueMsats(
rawValue,
revenueDisplayUnit,
usdPerSat
);
const compact = compactNumber.format(converted);
if (revenueDisplayUnit === 'usd') {
return `$${compact}`;
}
return `${compact} ${revenueUnitLabel}`;
};
const activeSeries = series.map((item) => ({
dataKey:
mode === 'requests'
? item.requestsKey
: mode === 'revenue'
? item.revenueKey
: item.tokensKey,
name: item.label,
color: item.color,
}));
const othersKey = (
mode === 'requests'
? 'others_requests'
: mode === 'revenue'
? 'others_revenue_msats'
: 'others_tokens'
) as 'others_requests' | 'others_revenue_msats' | 'others_tokens';
const activeSeriesKeys = [
...activeSeries.map((item) => item.dataKey),
othersKey,
];
const activeHoverSeriesKey =
hoveredSeriesKey && activeSeriesKeys.includes(hoveredSeriesKey)
? hoveredSeriesKey
: null;
const getSeriesOpacity = (dataKey: string): number =>
activeHoverSeriesKey && activeHoverSeriesKey !== dataKey ? 0.18 : 1;
const formatLeaderboardTotal = (rawValue: number): string => {
if (mode === 'requests') {
return `${compactNumber.format(rawValue)} requests`;
}
if (mode === 'tokens') {
return `${compactNumber.format(rawValue)} tokens`;
}
const converted = convertRevenueMsats(
rawValue,
revenueDisplayUnit,
usdPerSat
);
const compact = compactNumber.format(converted);
if (revenueDisplayUnit === 'usd') {
return `$${compact}`;
}
return `${compact} ${revenueUnitLabel}`;
};
const formatTrendPercent = (value: number): string => {
const abs = Math.abs(value);
const rounded = abs >= 10 ? abs.toFixed(0) : abs.toFixed(1);
return rounded.replace(/\.0$/, '');
};
const leaderboardRows = useMemo<LeaderboardRow[]>(() => {
if (leaderboardModels.length === 0 || mixMetrics.length === 0) {
return [];
}
const windowSize = Math.floor(mixMetrics.length / 2);
const previousMetrics =
windowSize > 0 ? mixMetrics.slice(-windowSize * 2, -windowSize) : [];
const currentMetrics =
windowSize > 0 ? mixMetrics.slice(-windowSize) : mixMetrics;
const rows = leaderboardModels
.map((model) => {
const readMetric = (metric: (typeof mixMetrics)[number]): number =>
mode === 'requests'
? ((metric.model_counts ?? {})[model] ?? 0)
: mode === 'revenue'
? ((metric.model_revenue_msats ?? {})[model] ?? 0)
: ((metric.model_tokens ?? {})[model] ?? 0);
const totalRaw = mixMetrics.reduce(
(sum, metric) => sum + readMetric(metric),
0
);
const previousRaw = previousMetrics.reduce(
(sum, metric) => sum + readMetric(metric),
0
);
const currentRaw = currentMetrics.reduce(
(sum, metric) => sum + readMetric(metric),
0
);
const trendPercent =
previousRaw > 0
? ((currentRaw - previousRaw) / previousRaw) * 100
: null;
let trend: LeaderboardTrend = 'flat';
if (previousRaw <= 0 && currentRaw > 0) {
trend = 'new';
} else if (trendPercent !== null && trendPercent > 0.5) {
trend = 'up';
} else if (trendPercent !== null && trendPercent < -0.5) {
trend = 'down';
}
const presentation = getModelPresentation(model);
const matchingSeries = series.find((item) => item.label === model);
const chartDataKey = matchingSeries
? mode === 'requests'
? matchingSeries.requestsKey
: mode === 'revenue'
? matchingSeries.revenueKey
: matchingSeries.tokensKey
: null;
return {
chartDataKey,
displayName: presentation.displayName,
model,
provider: presentation.provider,
rank: 0,
totalRaw,
trend,
trendPercent,
} satisfies LeaderboardRow;
})
.filter((row) => row.totalRaw > 0)
.sort((a, b) => b.totalRaw - a.totalRaw)
.slice(0, 20)
.map((row, index) => ({
...row,
rank: index + 1,
}));
return rows;
}, [leaderboardModels, mixMetrics, mode, series]);
const visibleLeaderboardRows = useMemo(
() =>
isMobile && !showAllMobileRows
? leaderboardRows.slice(0, 10)
: leaderboardRows,
[isMobile, leaderboardRows, showAllMobileRows]
);
const hiddenLeaderboardCount = Math.max(
0,
leaderboardRows.length - visibleLeaderboardRows.length
);
useEffect(() => {
setShowAllMobileRows(false);
}, [isMobile, mode, mixMetrics.length]);
if (chartData.length === 0) {
return null;
}
return (
<div ref={containerRef}>
<Card
className={cn(isFullscreen && 'h-full rounded-none border-0 ring-0')}
>
<CardHeader className='space-y-3 sm:space-y-4'>
<div className='flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between'>
<div className='min-w-0'>
<CardTitle className='text-base sm:text-lg'>
Model Usage
</CardTitle>
<p className='text-muted-foreground mt-1 text-xs sm:text-sm'>
Stacked requests, revenue, or tokens by model (
{mix.interval_minutes}m buckets).
</p>
</div>
<div className='flex items-center gap-2 sm:shrink-0'>
<div className='bg-muted/25 border-border/60 flex items-center gap-1 rounded-full border p-1'>
<Button
type='button'
size='sm'
variant={mode === 'requests' ? 'secondary' : 'ghost'}
onClick={() => setMode('requests')}
className='h-7 rounded-full px-2.5 text-xs'
>
Requests
</Button>
<Button
type='button'
size='sm'
variant={mode === 'revenue' ? 'secondary' : 'ghost'}
onClick={() => setMode('revenue')}
className='h-7 rounded-full px-2.5 text-xs'
>
Revenue
</Button>
<Button
type='button'
size='sm'
variant={mode === 'tokens' ? 'secondary' : 'ghost'}
onClick={() => setMode('tokens')}
className='h-7 rounded-full px-2.5 text-xs'
>
Tokens
</Button>
</div>
<Button
type='button'
variant='outline'
size='icon'
className='hidden h-8 w-8 shrink-0 sm:inline-flex'
onClick={toggleFullscreen}
>
{isFullscreen ? (
<Minimize2Icon className='h-4 w-4' />
) : (
<ExpandIcon className='h-4 w-4' />
)}
<span className='sr-only'>
{isFullscreen
? 'Exit fullscreen chart'
: 'Enter fullscreen chart'}
</span>
</Button>
</div>
</div>
</CardHeader>
<CardContent className='space-y-3 sm:space-y-4'>
<ChartContainer
config={chartConfig}
className={cn(
'aspect-auto w-full',
isFullscreen
? 'h-[calc(100vh-220px)] min-h-[340px] sm:h-[calc(100vh-260px)] sm:min-h-[420px]'
: 'h-[260px] sm:h-[340px]'
)}
onMouseLeave={() => {
setHoveredSeriesKey(null);
setIsChartPointerInside(false);
}}
>
<BarChart
data={chartData}
onMouseEnter={() => setIsChartPointerInside(true)}
onMouseMove={() => setIsChartPointerInside(true)}
onMouseLeave={() => {
setHoveredSeriesKey(null);
setIsChartPointerInside(false);
}}
margin={{
top: 12,
right: isMobile ? 8 : 18,
left: isMobile ? 0 : 8,
bottom: 0,
}}
>
<CartesianGrid vertical={false} className='stroke-muted/30' />
<XAxis
dataKey='timestamp'
tickLine={false}
axisLine={false}
minTickGap={isMobile ? 14 : 24}
tickFormatter={(value) =>
formatAxisTimestamp(
String(value),
hasMultipleDays,
mix.interval_minutes,
mix.hours_back
)
}
/>
<YAxis
tickLine={false}
axisLine={false}
width={isMobile ? 40 : 56}
tickFormatter={(value) =>
formatValue(
typeof value === 'number' ? value : Number(value || 0)
)
}
/>
<ChartTooltip
cursor={false}
content={({ active, payload, label }) => {
if (!isChartPointerInside || !active || !payload?.length) {
return null;
}
const rows = payload
.map((entry) => {
const value =
typeof entry.value === 'number'
? entry.value
: Number(entry.value || 0);
return {
color: String(entry.color || '#6b7280'),
dataKey: String(entry.dataKey || ''),
label: String(entry.name || ''),
value,
} satisfies TooltipRow;
})
.filter(
(row) => Number.isFinite(row.value) && row.value > 0
)
.sort((a, b) => b.value - a.value);
const total = rows.reduce((sum, row) => sum + row.value, 0);
if (rows.length === 0) {
return null;
}
return (
<div className='border-border/50 bg-background min-w-[220px] rounded-lg border px-2.5 py-2 text-xs shadow-xl'>
<p className='text-foreground mb-2 text-sm font-medium'>
{formatTooltipTimestamp(
String(label || ''),
mix.interval_minutes,
mix.hours_back
)}
</p>
<div className='space-y-1.5'>
{rows.map((row) => (
<div
key={row.label}
className={cn(
'grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3',
activeHoverSeriesKey &&
row.dataKey !== activeHoverSeriesKey &&
'opacity-45'
)}
>
<span className='text-muted-foreground flex min-w-0 items-center gap-2'>
<span
className='h-2.5 w-1.5 shrink-0 rounded-sm'
style={{ backgroundColor: row.color }}
/>
<span className='truncate'>{row.label}</span>
</span>
<span className='text-foreground font-mono tabular-nums'>
{formatValue(row.value)}
</span>
</div>
))}
</div>
<div className='border-border/60 mt-2 border-t pt-2'>
<div className='grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3'>
<span className='text-muted-foreground'>Total</span>
<span className='text-foreground font-mono font-semibold tabular-nums'>
{formatValue(total)}
</span>
</div>
</div>
</div>
);
}}
/>
{activeSeries.map((item) => (
<Bar
key={item.dataKey}
dataKey={item.dataKey}
name={item.name}
stackId='models'
fill={item.color}
fillOpacity={getSeriesOpacity(item.dataKey)}
maxBarSize={44}
onMouseEnter={() => setHoveredSeriesKey(item.dataKey)}
onMouseLeave={() => setHoveredSeriesKey(null)}
/>
))}
<Bar
dataKey={othersKey}
name='Others'
stackId='models'
fill='#6b7280'
fillOpacity={getSeriesOpacity(othersKey)}
maxBarSize={44}
onMouseEnter={() => setHoveredSeriesKey(othersKey)}
onMouseLeave={() => setHoveredSeriesKey(null)}
/>
</BarChart>
</ChartContainer>
<div className='border-border/60 space-y-2 border-t pt-3 sm:pt-4'>
<div className='flex items-center justify-between gap-3'>
<p className='text-muted-foreground text-xs font-medium'>
Top models
</p>
<p className='text-muted-foreground hidden text-xs sm:block'>
Change vs prior period
</p>
</div>
{leaderboardRows.length > 0 ? (
<div className='divide-border/40 divide-y'>
{visibleLeaderboardRows.map((row) => {
const rowIsLinked = Boolean(row.chartDataKey);
const rowIsActive =
row.chartDataKey !== null &&
activeHoverSeriesKey === row.chartDataKey;
const rowIsDimmed =
Boolean(activeHoverSeriesKey) &&
row.chartDataKey !== null &&
row.chartDataKey !== activeHoverSeriesKey;
let trendLabel = '0%';
let trendClass = 'text-muted-foreground';
if (row.trend === 'new') {
trendLabel = 'new';
trendClass = 'text-blue-500';
} else if (row.trend === 'up' && row.trendPercent !== null) {
trendLabel = `↑${formatTrendPercent(row.trendPercent)}%`;
trendClass = 'text-emerald-500';
} else if (
row.trend === 'down' &&
row.trendPercent !== null
) {
trendLabel = `↓${formatTrendPercent(row.trendPercent)}%`;
trendClass = 'text-red-500';
} else if (row.trendPercent !== null) {
trendLabel = `${formatTrendPercent(row.trendPercent)}%`;
}
return (
<div
key={row.model}
className={cn(
'grid rounded-md px-2 py-2 text-xs',
isMobile
? 'grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-x-2.5'
: 'grid-cols-[auto_minmax(0,1fr)_auto_auto] items-center',
rowIsLinked &&
'hover:bg-muted/25 cursor-pointer transition',
rowIsActive && 'bg-muted/30',
rowIsDimmed && 'opacity-45'
)}
title={row.model}
onMouseEnter={() => {
if (row.chartDataKey) {
setHoveredSeriesKey(row.chartDataKey);
}
}}
onMouseLeave={() => {
if (row.chartDataKey) {
setHoveredSeriesKey(null);
}
}}
>
<span
className={cn(
'text-muted-foreground w-5 text-right font-mono tabular-nums',
isMobile && 'pt-0.5 text-[11px]'
)}
>
{row.rank}.
</span>
{isMobile ? (
<>
<div className='min-w-0 pt-0.5'>
<p className='leading-snug break-words'>
<span className='font-medium'>
{row.displayName}
</span>{' '}
<span className='text-muted-foreground'>
by {row.provider}
</span>
</p>
</div>
<div className='flex min-w-[5.75rem] flex-col items-end gap-1 text-right'>
<span className='text-foreground font-mono tabular-nums'>
{formatLeaderboardTotal(row.totalRaw)}
</span>
<span
className={cn(
'shrink-0 text-[11px] font-medium',
trendClass
)}
>
{trendLabel}
</span>
</div>
</>
) : (
<>
<div className='min-w-0'>
<span className='truncate font-medium'>
{row.displayName}
</span>{' '}
<span className='text-muted-foreground truncate'>
by {row.provider}
</span>
</div>
<span className='text-foreground font-mono tabular-nums'>
{formatLeaderboardTotal(row.totalRaw)}
</span>
<span
className={cn('text-right font-medium', trendClass)}
>
{trendLabel}
</span>
</>
)}
</div>
);
})}
</div>
) : (
<p className='text-muted-foreground text-xs'>
No model totals available for this range.
</p>
)}
{isMobile && hiddenLeaderboardCount > 0 ? (
<Button
type='button'
variant='ghost'
size='sm'
className='text-muted-foreground hover:text-foreground h-8 px-0 text-xs font-medium'
onClick={() => setShowAllMobileRows(true)}
>
Show {hiddenLeaderboardCount} more
</Button>
) : null}
{isMobile && showAllMobileRows && leaderboardRows.length > 10 ? (
<Button
type='button'
variant='ghost'
size='sm'
className='text-muted-foreground hover:text-foreground h-8 px-0 text-xs font-medium'
onClick={() => setShowAllMobileRows(false)}
>
Show less
</Button>
) : null}
</div>
</CardContent>
</Card>
</div>
);
}
-22
View File
@@ -1045,33 +1045,11 @@ export interface RevenueByModel {
total_models: number;
}
export interface ModelUsageMixMetric {
timestamp: string;
total_successful: number;
total_revenue_msats: number;
total_tokens: number;
others: number;
others_revenue_msats: number;
others_tokens: number;
model_counts: Record<string, number>;
model_revenue_msats: Record<string, number>;
model_tokens: Record<string, number>;
}
export interface ModelUsageMix {
top_models: string[];
metrics: ModelUsageMixMetric[];
interval_minutes: number;
hours_back: number;
total_buckets: number;
}
export interface UsageDashboardResponse {
metrics: UsageMetrics;
summary: UsageSummary;
error_details: ErrorDetails;
revenue_by_model: RevenueByModel;
model_usage_mix?: ModelUsageMix;
}
export interface LogEntry {