mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-10-05 20:28:23 +00:00
Remove model usage analytics from UI update branch
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user