diff --git a/routstr/core/log_manager.py b/routstr/core/log_manager.py index 0444dcbf..46015e0b 100644 --- a/routstr/core/log_manager.py +++ b/routstr/core/log_manager.py @@ -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( diff --git a/routstr/core/usage_analytics_store.py b/routstr/core/usage_analytics_store.py index 7ba90e24..a51f5bb8 100644 --- a/routstr/core/usage_analytics_store.py +++ b/routstr/core/usage_analytics_store.py @@ -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") diff --git a/ui/app/page.tsx b/ui/app/page.tsx index 305daf1c..f7f38c96 100644 --- a/ui/app/page.tsx +++ b/ui/app/page.tsx @@ -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() { )} - {!metricsLoading && modelUsageMixData && hasModelUsageMixMetrics ? ( - - ) : null} - {summaryLoading ? ( ) : summaryData ? ( diff --git a/ui/components/top-models-usage-chart.tsx b/ui/components/top-models-usage-chart.tsx deleted file mode 100644 index 716b4f73..00000000 --- a/ui/components/top-models-usage-chart.tsx +++ /dev/null @@ -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 = { - '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('requests'); - const [hoveredSeriesKey, setHoveredSeriesKey] = useState(null); - const [isChartPointerInside, setIsChartPointerInside] = useState(false); - const [isFullscreen, setIsFullscreen] = useState(false); - const [showAllMobileRows, setShowAllMobileRows] = useState(false); - const isMobile = useIsMobile(); - const containerRef = useRef(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 = { - 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(() => { - 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 ( -
- - -
-
- - Model Usage - -

- Stacked requests, revenue, or tokens by model ( - {mix.interval_minutes}m buckets). -

-
-
-
- - - -
- -
-
-
- - { - setHoveredSeriesKey(null); - setIsChartPointerInside(false); - }} - > - setIsChartPointerInside(true)} - onMouseMove={() => setIsChartPointerInside(true)} - onMouseLeave={() => { - setHoveredSeriesKey(null); - setIsChartPointerInside(false); - }} - margin={{ - top: 12, - right: isMobile ? 8 : 18, - left: isMobile ? 0 : 8, - bottom: 0, - }} - > - - - formatAxisTimestamp( - String(value), - hasMultipleDays, - mix.interval_minutes, - mix.hours_back - ) - } - /> - - formatValue( - typeof value === 'number' ? value : Number(value || 0) - ) - } - /> - { - 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 ( -
-

- {formatTooltipTimestamp( - String(label || ''), - mix.interval_minutes, - mix.hours_back - )} -

-
- {rows.map((row) => ( -
- - - {row.label} - - - {formatValue(row.value)} - -
- ))} -
-
-
- Total - - {formatValue(total)} - -
-
-
- ); - }} - /> - {activeSeries.map((item) => ( - setHoveredSeriesKey(item.dataKey)} - onMouseLeave={() => setHoveredSeriesKey(null)} - /> - ))} - setHoveredSeriesKey(othersKey)} - onMouseLeave={() => setHoveredSeriesKey(null)} - /> -
-
- -
-
-

- Top models -

-

- Change vs prior period -

-
- - {leaderboardRows.length > 0 ? ( -
- {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 ( -
{ - if (row.chartDataKey) { - setHoveredSeriesKey(row.chartDataKey); - } - }} - onMouseLeave={() => { - if (row.chartDataKey) { - setHoveredSeriesKey(null); - } - }} - > - - {row.rank}. - - {isMobile ? ( - <> -
-

- - {row.displayName} - {' '} - - by {row.provider} - -

-
-
- - {formatLeaderboardTotal(row.totalRaw)} - - - {trendLabel} - -
- - ) : ( - <> -
- - {row.displayName} - {' '} - - by {row.provider} - -
- - {formatLeaderboardTotal(row.totalRaw)} - - - {trendLabel} - - - )} -
- ); - })} -
- ) : ( -

- No model totals available for this range. -

- )} - - {isMobile && hiddenLeaderboardCount > 0 ? ( - - ) : null} - - {isMobile && showAllMobileRows && leaderboardRows.length > 10 ? ( - - ) : null} -
-
-
-
- ); -} diff --git a/ui/lib/api/services/admin.ts b/ui/lib/api/services/admin.ts index b3f7677f..b8c22bca 100644 --- a/ui/lib/api/services/admin.ts +++ b/ui/lib/api/services/admin.ts @@ -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; - model_revenue_msats: Record; - model_tokens: Record; -} - -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 {