"use client"; import { useState, useEffect, useMemo, useCallback, Fragment } from "react"; import PropTypes from "prop-types"; import { useSearchParams, useRouter } from "next/navigation"; import Card from "./Card"; import Badge from "./Badge"; import { CardSkeleton } from "./Loading"; function SortIcon({ field, currentSort, currentOrder }) { if (currentSort !== field) return ↕; return {currentOrder === "asc" ? "↑" : "↓"}; } SortIcon.propTypes = { field: PropTypes.string.isRequired, currentSort: PropTypes.string.isRequired, currentOrder: PropTypes.string.isRequired, }; function MiniBarGraph({ data, colorClass = "bg-primary" }) { const max = Math.max(...data, 1); return (
{data.slice(-9).map((val, idx) => (
))}
); } MiniBarGraph.propTypes = { data: PropTypes.arrayOf(PropTypes.number).isRequired, colorClass: PropTypes.string, }; export default function UsageStats() { const router = useRouter(); const searchParams = useSearchParams(); const modelSortBy = searchParams.get("modelSortBy") || "rawModel"; const modelSortOrder = searchParams.get("modelSortOrder") || "asc"; const accountSortBy = searchParams.get("accountSortBy") || "rawModel"; const accountSortOrder = searchParams.get("accountSortOrder") || "asc"; const apiKeySortBy = searchParams.get("apiKeySortBy") || "keyName"; const apiKeySortOrder = searchParams.get("apiKeySortOrder") || "asc"; const endpointSortBy = searchParams.get("endpointSortBy") || "endpoint"; const endpointSortOrder = searchParams.get("endpointSortOrder") || "asc"; const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const [autoRefresh, setAutoRefresh] = useState(true); const [viewMode, setViewMode] = useState("tokens"); // 'tokens' or 'costs' const [refreshInterval, setRefreshInterval] = useState(5000); // Start with 5s const [prevTotalRequests, setPrevTotalRequests] = useState(0); const [expandedModels, setExpandedModels] = useState(new Set()); const [expandedAccounts, setExpandedAccounts] = useState(new Set()); const [expandedApiKeys, setExpandedApiKeys] = useState(new Set()); const [expandedEndpoints, setExpandedEndpoints] = useState(new Set()); const toggleSort = (tableType, field) => { const sortKeyMap = { model: { by: "modelSortBy", order: "modelSortOrder" }, account: { by: "accountSortBy", order: "accountSortOrder" }, apiKey: { by: "apiKeySortBy", order: "apiKeySortOrder" }, endpoint: { by: "endpointSortBy", order: "endpointSortOrder" } }; const sortKeys = sortKeyMap[tableType]; const params = new URLSearchParams(searchParams.toString()); const currentBy = params.get(sortKeys.by); const currentOrder = params.get(sortKeys.order); if (currentBy === field) { params.set(sortKeys.order, currentOrder === "asc" ? "desc" : "asc"); } else { params.set(sortKeys.by, field); params.set(sortKeys.order, "asc"); } router.replace(`?${params.toString()}`, { scroll: false }); }; const sortData = useCallback((dataMap, pendingMap = {}, sortBy, sortOrder) => { return Object.entries(dataMap || {}) .map(([key, data]) => { const totalTokens = (data.promptTokens || 0) + (data.completionTokens || 0); const totalCost = data.cost || 0; const inputCost = totalTokens > 0 ? (data.promptTokens || 0) * (totalCost / totalTokens) : 0; const outputCost = totalTokens > 0 ? (data.completionTokens || 0) * (totalCost / totalTokens) : 0; return { ...data, key, totalTokens, totalCost, inputCost, outputCost, pending: pendingMap[key] || 0, }; }) .sort((a, b) => { let valA = a[sortBy]; let valB = b[sortBy]; if (typeof valA === "string") valA = valA.toLowerCase(); if (typeof valB === "string") valB = valB.toLowerCase(); if (valA < valB) return sortOrder === "asc" ? -1 : 1; if (valA > valB) return sortOrder === "asc" ? 1 : -1; return 0; }); }, []); /** * Extract grouping key from data item based on field type * @param {object} item - Data item with usage stats * @param {string} keyField - Field to use for grouping ('rawModel', 'accountName', 'keyName') * @returns {string} - Grouping key value */ const getGroupKey = useCallback((item, keyField) => { switch (keyField) { case 'rawModel': return item.rawModel || 'Unknown Model'; case 'accountName': return item.accountName || `Account ${item.connectionId?.slice(0, 8)}...` || 'Unknown Account'; case 'keyName': return item.keyName || 'Unknown Key'; case 'endpoint': return item.endpoint || 'Unknown Endpoint'; default: return item[keyField] || 'Unknown'; } }, []); /** * Group flat data array by key field and calculate aggregated values * @param {Array} data - Flat array of data items (output from sortData) * @param {string} keyField - Field to use for grouping ('rawModel', 'accountName', 'keyName') * @returns {Array} - Array of groups with summary and items */ const groupDataByKey = useCallback((data, keyField) => { if (!Array.isArray(data)) return []; const groups = {}; data.forEach((item) => { const groupKey = getGroupKey(item, keyField); if (!groups[groupKey]) { groups[groupKey] = { groupKey, summary: { requests: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0, cost: 0, inputCost: 0, outputCost: 0, lastUsed: null, pending: 0 }, items: [] }; } const group = groups[groupKey]; // Aggregate values: sum for most, max for lastUsed group.summary.requests += item.requests || 0; group.summary.promptTokens += item.promptTokens || 0; group.summary.completionTokens += item.completionTokens || 0; group.summary.totalTokens += item.totalTokens || 0; group.summary.cost += item.cost || 0; group.summary.inputCost += item.inputCost || 0; group.summary.outputCost += item.outputCost || 0; group.summary.pending += item.pending || 0; // Take max for lastUsed (most recent timestamp) if (item.lastUsed) { if (!group.summary.lastUsed || new Date(item.lastUsed) > new Date(group.summary.lastUsed)) { group.summary.lastUsed = item.lastUsed; } } // Add item to group group.items.push(item); }); return Object.values(groups); }, [getGroupKey]); const sortedModels = useMemo( () => sortData(stats?.byModel, stats?.pending?.byModel, modelSortBy, modelSortOrder), [stats?.byModel, stats?.pending?.byModel, modelSortBy, modelSortOrder, sortData] ); const groupedModels = useMemo( () => groupDataByKey(sortedModels, 'rawModel'), [sortedModels, groupDataByKey] ); const sortedAccounts = useMemo(() => { const accountPendingMap = {}; if (stats?.pending?.byAccount) { Object.entries(stats.byAccount || {}).forEach(([accountKey, data]) => { const connPending = stats.pending.byAccount[data.connectionId]; if (connPending) { const modelKey = data.provider ? `${data.rawModel} (${data.provider})` : data.rawModel; accountPendingMap[accountKey] = connPending[modelKey] || 0; } }); } return sortData(stats?.byAccount, accountPendingMap, accountSortBy, accountSortOrder); }, [stats?.byAccount, stats?.pending?.byAccount, accountSortBy, accountSortOrder, sortData]); const groupedAccounts = useMemo( () => groupDataByKey(sortedAccounts, 'accountName'), [sortedAccounts, groupDataByKey] ); const sortedApiKeys = useMemo(() => sortData(stats?.byApiKey, {}, apiKeySortBy, apiKeySortOrder), [stats?.byApiKey, apiKeySortBy, apiKeySortOrder, sortData]); const groupedApiKeys = useMemo( () => groupDataByKey(sortedApiKeys, 'keyName'), [sortedApiKeys, groupDataByKey] ); const sortedEndpoints = useMemo( () => sortData(stats?.byEndpoint, {}, endpointSortBy, endpointSortOrder), [stats?.byEndpoint, endpointSortBy, endpointSortOrder, sortData] ); const groupedEndpoints = useMemo( () => groupDataByKey(sortedEndpoints, 'endpoint'), [sortedEndpoints, groupDataByKey] ); const fetchStats = useCallback(async (showLoading = true) => { if (showLoading) setLoading(true); try { const res = await fetch("/api/usage/history"); if (res.ok) { const data = await res.json(); setStats(data); // Smart polling: adjust interval based on activity const currentTotal = data.totalRequests || 0; if (currentTotal > prevTotalRequests) { // New requests detected - reset to fast polling setRefreshInterval(5000); } else { // No change - increase interval (exponential backoff) setRefreshInterval((prev) => Math.min(prev * 2, 60000)); // Max 60s } setPrevTotalRequests(currentTotal); } } catch (error) { console.error("Failed to fetch usage stats:", error); } finally { if (showLoading) setLoading(false); } }, [prevTotalRequests]); useEffect(() => { fetchStats(); }, [fetchStats]); useEffect(() => { try { const saved = localStorage.getItem('usage-stats:expanded-models'); if (saved) { setExpandedModels(new Set(JSON.parse(saved))); } } catch (error) { console.error("Failed to load expanded models from localStorage:", error); } }, []); useEffect(() => { try { localStorage.setItem('usage-stats:expanded-models', JSON.stringify([...expandedModels])); } catch (error) { console.error("Failed to save expanded models to localStorage:", error); } }, [expandedModels]); useEffect(() => { try { const saved = localStorage.getItem('usage-stats:expanded-accounts'); if (saved) { setExpandedAccounts(new Set(JSON.parse(saved))); } } catch (error) { console.error("Failed to load expanded accounts from localStorage:", error); } }, []); useEffect(() => { try { localStorage.setItem('usage-stats:expanded-accounts', JSON.stringify([...expandedAccounts])); } catch (error) { console.error("Failed to save expanded accounts to localStorage:", error); } }, [expandedAccounts]); useEffect(() => { try { const saved = localStorage.getItem('usage-stats:expanded-apikeys'); if (saved) { setExpandedApiKeys(new Set(JSON.parse(saved))); } } catch (error) { console.error("Failed to load expanded API keys from localStorage:", error); } }, []); useEffect(() => { try { localStorage.setItem('usage-stats:expanded-apikeys', JSON.stringify([...expandedApiKeys])); } catch (error) { console.error("Failed to save expanded API keys to localStorage:", error); } }, [expandedApiKeys]); useEffect(() => { try { const saved = localStorage.getItem('usage-stats:expanded-endpoints'); if (saved) { setExpandedEndpoints(new Set(JSON.parse(saved))); } } catch (error) { console.error("Failed to load expanded endpoints from localStorage:", error); } }, []); useEffect(() => { try { localStorage.setItem('usage-stats:expanded-endpoints', JSON.stringify([...expandedEndpoints])); } catch (error) { console.error("Failed to save expanded endpoints to localStorage:", error); } }, [expandedEndpoints]); const toggleModelGroup = useCallback((groupKey) => { setExpandedModels(prev => { const next = new Set(prev); if (next.has(groupKey)) { next.delete(groupKey); } else { next.add(groupKey); } return next; }); }, []); const toggleAccountGroup = useCallback((groupKey) => { setExpandedAccounts(prev => { const next = new Set(prev); if (next.has(groupKey)) { next.delete(groupKey); } else { next.add(groupKey); } return next; }); }, []); const toggleApiKeyGroup = useCallback((groupKey) => { setExpandedApiKeys(prev => { const next = new Set(prev); if (next.has(groupKey)) { next.delete(groupKey); } else { next.add(groupKey); } return next; }); }, []); const toggleEndpointGroup = useCallback((groupKey) => { setExpandedEndpoints(prev => { const next = new Set(prev); if (next.has(groupKey)) { next.delete(groupKey); } else { next.add(groupKey); } return next; }); }, []); useEffect(() => { let intervalId; let isPageVisible = true; // Page Visibility API - pause when tab is hidden const handleVisibilityChange = () => { isPageVisible = !document.hidden; if (isPageVisible && autoRefresh) { fetchStats(false); // Fetch immediately when tab becomes visible } }; document.addEventListener("visibilitychange", handleVisibilityChange); if (autoRefresh) { // Clear any existing interval first if (intervalId) clearInterval(intervalId); intervalId = setInterval(() => { if (isPageVisible) { fetchStats(false); // fetch without loading skeleton } }, refreshInterval); } return () => { if (intervalId) clearInterval(intervalId); document.removeEventListener("visibilitychange", handleVisibilityChange); }; }, [autoRefresh, refreshInterval, fetchStats]); if (loading) return ; if (!stats) return (
Failed to load usage statistics.
); // Format number with commas const fmt = (n) => new Intl.NumberFormat().format(n || 0); // Format cost with dollar sign and 2 decimals const fmtCost = (n) => `$${(n || 0).toFixed(2)}`; // Time format for "Last Used" const fmtTime = (iso) => { if (!iso) return "Never"; const date = new Date(iso); const now = new Date(); const diffMs = now - date; const diffMins = Math.floor(diffMs / 60000); if (diffMins < 1) return "Just now"; if (diffMins < 60) return `${diffMins}m ago`; if (diffMins < 1440) return `${Math.floor(diffMins / 60)}h ago`; return date.toLocaleDateString(); }; return (
{/* Header with Auto Refresh Toggle and View Toggle */}

Usage Overview

{/* View Toggle */}
{/* Auto Refresh Toggle */}
Auto Refresh ({refreshInterval / 1000}s)
{/* Active Requests Summary */} {(stats.activeRequests || []).length > 0 && (
Active Requests
{stats.activeRequests.map((req) => (
{req.model} | {req.provider} | {req.account} {req.count > 1 && ( x{req.count} )}
))}
)} {/* Overview Cards */}
Total Requests {fmt(stats.totalRequests)}
m.requests)} colorClass="bg-text-muted/30" />
Total Input Tokens {fmt(stats.totalPromptTokens)}
m.promptTokens)} colorClass="bg-primary/50" />
Output Tokens {fmt(stats.totalCompletionTokens)}
Total Cost {fmtCost(stats.totalCost)}
{/* Usage by Model Table */}

Usage by Model

{viewMode === "tokens" ? ( <> ) : ( <> )} {groupedModels.map((group) => ( toggleModelGroup(group.groupKey)} > {viewMode === "tokens" ? ( <> ) : ( <> )} {expandedModels.has(group.groupKey) && group.items.map((item) => ( {viewMode === "tokens" ? ( <> ) : ( <> )} ))} ))} {groupedModels.length === 0 && ( )}
toggleSort("model", "rawModel")} > Model{" "} toggleSort("model", "provider")} > Provider{" "} toggleSort("model", "requests")} > Requests{" "} toggleSort("model", "lastUsed")} > Last Used{" "} toggleSort("model", "promptTokens")} > Input Tokens{" "} toggleSort("model", "completionTokens")} > Output Tokens{" "} toggleSort("model", "totalTokens")} > Total Tokens{" "} toggleSort("model", "promptTokens")} > Input Cost{" "} toggleSort("model", "completionTokens")} > Output Cost{" "} toggleSort("model", "cost")} > Total Cost{" "}
chevron_right 0 ? "text-primary" : ""}`}> {group.groupKey}
— {fmt(group.summary.requests)} {fmtTime(group.summary.lastUsed)} {fmt(group.summary.promptTokens)} {fmt(group.summary.completionTokens)} {fmt(group.summary.totalTokens)} {fmtCost(group.summary.inputCost)} {fmtCost(group.summary.outputCost)} {fmtCost(group.summary.totalCost)}
0 ? "text-primary" : "" }`} > {item.rawModel} 0 ? "primary" : "neutral"} size="sm" > {item.provider} {fmt(item.requests)} {fmtTime(item.lastUsed)} {fmt(item.promptTokens)} {fmt(item.completionTokens)} {fmt(item.totalTokens)} {fmtCost(item.inputCost)} {fmtCost(item.outputCost)} {fmtCost(item.totalCost)}
No usage recorded yet. Make some requests to see data here.
{/* Usage by Account Table */}

Usage by Account

{viewMode === "tokens" ? ( <> ) : ( <> )} {groupedAccounts.map((group) => ( toggleAccountGroup(group.groupKey)} > {viewMode === "tokens" ? ( <> ) : ( <> )} {expandedAccounts.has(group.groupKey) && group.items.map((item) => ( {viewMode === "tokens" ? ( <> ) : ( <> )} ))} ))} {groupedAccounts.length === 0 && ( )}
toggleSort("account", "rawModel")} > Model{" "} toggleSort("account", "provider")} > Provider{" "} toggleSort("account", "accountName")} > Account{" "} toggleSort("account", "requests")} > Requests{" "} toggleSort("account", "lastUsed")} > Last Used{" "} toggleSort("account", "promptTokens")} > Input Tokens{" "} toggleSort("account", "completionTokens")} > Output Tokens{" "} toggleSort("account", "totalTokens")} > Total Tokens{" "} toggleSort("account", "promptTokens")} > Input Cost{" "} toggleSort("account", "completionTokens")} > Output Cost{" "} toggleSort("account", "cost")} > Total Cost{" "}
chevron_right 0 ? "text-primary" : ""}`}> {group.groupKey}
— — {fmt(group.summary.requests)} {fmtTime(group.summary.lastUsed)} — — {fmt(group.summary.totalTokens)} — — {fmtCost(group.summary.totalCost)}
0 ? "text-primary" : "" }`} > {item.accountName || `Account ${item.connectionId?.slice(0, 8)}...`} 0 ? "text-primary" : "" }`} > {item.rawModel} 0 ? "primary" : "neutral"} size="sm" > {item.provider} {fmt(item.requests)} {fmtTime(item.lastUsed)} {fmt(item.promptTokens)} {fmt(item.completionTokens)} {fmt(item.totalTokens)} {fmtCost(item.inputCost)} {fmtCost(item.outputCost)} {fmtCost(item.totalCost)}
No account-specific usage recorded yet. Make requests using OAuth accounts to see data here.

Usage by API Key

{viewMode === "tokens" ? ( <> ) : ( <> )} {groupedApiKeys.map((group) => ( toggleApiKeyGroup(group.groupKey)} > {viewMode === "tokens" ? ( <> ) : ( <> )} {expandedApiKeys.has(group.groupKey) && group.items.map((item) => ( {viewMode === "tokens" ? ( <> ) : ( <> )} ))} ))} {groupedApiKeys.length === 0 && ( )}
toggleSort("apiKey", "keyName")} > API Key Name{" "} toggleSort("apiKey", "rawModel")} > Model{" "} toggleSort("apiKey", "provider")} > Provider{" "} toggleSort("apiKey", "requests")} > Requests{" "} toggleSort("apiKey", "lastUsed")} > Last Used{" "} toggleSort("apiKey", "promptTokens")} > Input Tokens{" "} toggleSort("apiKey", "completionTokens")} > Output Tokens{" "} toggleSort("apiKey", "totalTokens")} > Total Tokens{" "} toggleSort("apiKey", "promptTokens")} > Input Cost{" "} toggleSort("apiKey", "completionTokens")} > Output Cost{" "} toggleSort("apiKey", "cost")} > Total Cost{" "}
chevron_right {group.groupKey}
— — {fmt(group.summary.requests)} {fmtTime(group.summary.lastUsed)} {fmt(group.summary.promptTokens)} {fmt(group.summary.completionTokens)} {fmt(group.summary.totalTokens)} {fmtCost(group.summary.inputCost)} {fmtCost(group.summary.outputCost)} {fmtCost(group.summary.totalCost)}
{item.keyName} {item.rawModel} {item.provider} {fmt(item.requests)} {fmtTime(item.lastUsed)} {fmt(item.promptTokens)} {fmt(item.completionTokens)} {fmt(item.totalTokens)} {fmtCost(item.inputCost)} {fmtCost(item.outputCost)} {fmtCost(item.totalCost)}
No API key usage recorded yet. Make requests using API keys to see data here.
{/* Usage by API Endpoint Table */}

Usage by API Endpoint

{viewMode === "tokens" ? ( <> ) : ( <> )} {groupedEndpoints.map((group) => ( toggleEndpointGroup(group.groupKey)} > {viewMode === "tokens" ? ( <> ) : ( <> )} {expandedEndpoints.has(group.groupKey) && group.items.map((item) => ( {viewMode === "tokens" ? ( <> ) : ( <> )} ))} ))} {groupedEndpoints.length === 0 && ( )}
toggleSort("endpoint", "endpoint")} > Endpoint{" "} toggleSort("endpoint", "rawModel")} > Model{" "} toggleSort("endpoint", "provider")} > Provider{" "} toggleSort("endpoint", "requests")} > Requests{" "} toggleSort("endpoint", "lastUsed")} > Last Used{" "} toggleSort("endpoint", "promptTokens")} > Input Tokens{" "} toggleSort("endpoint", "completionTokens")} > Output Tokens{" "} toggleSort("endpoint", "totalTokens")} > Total Tokens{" "} toggleSort("endpoint", "promptTokens")} > Input Cost{" "} toggleSort("endpoint", "completionTokens")} > Output Cost{" "} toggleSort("endpoint", "cost")} > Total Cost{" "}
chevron_right {group.groupKey}
— — {fmt(group.summary.requests)} {fmtTime(group.summary.lastUsed)} {fmt(group.summary.promptTokens)} {fmt(group.summary.completionTokens)} {fmt(group.summary.totalTokens)} {fmtCost(group.summary.inputCost)} {fmtCost(group.summary.outputCost)} {fmtCost(group.summary.totalCost)}
{item.endpoint} {item.rawModel} {item.provider} {fmt(item.requests)} {fmtTime(item.lastUsed)} {fmt(item.promptTokens)} {fmt(item.completionTokens)} {fmt(item.totalTokens)} {fmtCost(item.inputCost)} {fmtCost(item.outputCost)} {fmtCost(item.totalCost)}
No endpoint usage recorded yet. Make requests to see data here.
); }