From 19532064daa1176e3ef656640e66cab90eab04e9 Mon Sep 17 00:00:00 2001 From: Supra4E8C Date: Sun, 22 Feb 2026 18:48:01 +0800 Subject: [PATCH] fix(usage): stabilize dashboard metrics and reduce repeated parsing --- src/components/usage/CredentialStatsCard.tsx | 65 ++--- .../usage/RequestEventsDetailsCard.tsx | 7 +- src/components/usage/StatCards.tsx | 73 +++++- src/components/usage/hooks/useSparklines.ts | 111 +++++---- src/pages/UsagePage.tsx | 5 +- src/stores/useUsageStatsStore.ts | 7 +- src/utils/usage.ts | 226 +++++++++++++----- 7 files changed, 347 insertions(+), 147 deletions(-) diff --git a/src/components/usage/CredentialStatsCard.tsx b/src/components/usage/CredentialStatsCard.tsx index cd9b795c..268dc279 100644 --- a/src/components/usage/CredentialStatsCard.tsx +++ b/src/components/usage/CredentialStatsCard.tsx @@ -2,7 +2,6 @@ import { useMemo, useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { Card } from '@/components/ui/Card'; import { - computeKeyStats, collectUsageDetails, buildCandidateUsageSourceIds, formatCompactNumber, @@ -82,14 +81,49 @@ export function CredentialStatsCard({ // Auth files are used purely for name resolution of unmatched source IDs. const rows = useMemo((): CredentialRow[] => { if (!usage) return []; - const { bySource } = computeKeyStats(usage); const details = collectUsageDetails(usage); + const bySource: Record = {}; const result: CredentialRow[] = []; const consumedSourceIds = new Set(); const authIndexToRowIndex = new Map(); const sourceToAuthIndex = new Map(); + const sourceToAuthFile = new Map(); const fallbackByAuthIndex = new Map(); + details.forEach((detail) => { + const authIdx = normalizeAuthIndex(detail.auth_index); + const source = detail.source; + const isFailed = detail.failed === true; + + if (!source) { + if (!authIdx) return; + const fallback = fallbackByAuthIndex.get(authIdx) ?? { success: 0, failure: 0 }; + if (isFailed) { + fallback.failure += 1; + } else { + fallback.success += 1; + } + fallbackByAuthIndex.set(authIdx, fallback); + return; + } + + const bucket = bySource[source] ?? { success: 0, failure: 0 }; + if (isFailed) { + bucket.failure += 1; + } else { + bucket.success += 1; + } + bySource[source] = bucket; + + if (authIdx && !sourceToAuthIndex.has(source)) { + sourceToAuthIndex.set(source, authIdx); + } + if (authIdx && !sourceToAuthFile.has(source)) { + const mapped = authFileMap.get(authIdx); + if (mapped) sourceToAuthFile.set(source, mapped); + } + }); + const mergeBucketToRow = (index: number, bucket: CredentialBucket) => { const target = result[index]; if (!target) return; @@ -177,33 +211,6 @@ export function CredentialStatsCard({ } }); - // Build source → auth file name mapping for remaining unmatched entries. - // Also collect fallback stats for details without source but with auth_index. - const sourceToAuthFile = new Map(); - details.forEach((d) => { - const authIdx = normalizeAuthIndex(d.auth_index); - if (!d.source) { - if (!authIdx) return; - const fallback = fallbackByAuthIndex.get(authIdx) ?? { success: 0, failure: 0 }; - if (d.failed === true) { - fallback.failure += 1; - } else { - fallback.success += 1; - } - fallbackByAuthIndex.set(authIdx, fallback); - return; - } - - if (!authIdx || consumedSourceIds.has(d.source)) return; - if (!sourceToAuthIndex.has(d.source)) { - sourceToAuthIndex.set(d.source, authIdx); - } - if (!sourceToAuthFile.has(d.source)) { - const mapped = authFileMap.get(authIdx); - if (mapped) sourceToAuthFile.set(d.source, mapped); - } - }); - // Remaining unmatched bySource entries — resolve name from auth files if possible Object.entries(bySource).forEach(([key, bucket]) => { if (consumedSourceIds.has(key)) return; diff --git a/src/components/usage/RequestEventsDetailsCard.tsx b/src/components/usage/RequestEventsDetailsCard.tsx index 5dcfb35f..6049af2d 100644 --- a/src/components/usage/RequestEventsDetailsCard.tsx +++ b/src/components/usage/RequestEventsDetailsCard.tsx @@ -119,8 +119,11 @@ export function RequestEventsDetailsCard({ return details .map((detail, index) => { - const timestamp = typeof detail.timestamp === 'string' ? detail.timestamp : ''; - const timestampMs = Date.parse(timestamp); + const timestamp = detail.timestamp; + const timestampMs = + typeof detail.__timestampMs === 'number' && detail.__timestampMs > 0 + ? detail.__timestampMs + : Date.parse(timestamp); const date = Number.isNaN(timestampMs) ? null : new Date(timestampMs); const sourceRaw = String(detail.source ?? '').trim(); const authIndexRaw = detail.auth_index as unknown; diff --git a/src/components/usage/StatCards.tsx b/src/components/usage/StatCards.tsx index 43df7cff..9d80b74b 100644 --- a/src/components/usage/StatCards.tsx +++ b/src/components/usage/StatCards.tsx @@ -1,4 +1,4 @@ -import type { CSSProperties, ReactNode } from 'react'; +import { useMemo, type CSSProperties, type ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import { Line } from 'react-chartjs-2'; import { IconDiamond, IconDollarSign, IconSatellite, IconTimer, IconTrendingUp } from '@/components/ui/icons'; @@ -6,9 +6,9 @@ import { formatCompactNumber, formatPerMinuteValue, formatUsd, - calculateTokenBreakdown, - calculateRecentPerMinuteRates, - calculateTotalCost, + calculateCost, + collectUsageDetails, + extractTotalTokens, type ModelPrice } from '@/utils/usage'; import { sparklineOptions } from '@/utils/usage/chartConfig'; @@ -32,6 +32,7 @@ export interface StatCardsProps { usage: UsagePayload | null; loading: boolean; modelPrices: Record; + nowMs: number; sparklines: { requests: SparklineBundle | null; tokens: SparklineBundle | null; @@ -41,16 +42,68 @@ export interface StatCardsProps { }; } -export function StatCards({ usage, loading, modelPrices, sparklines }: StatCardsProps) { +export function StatCards({ usage, loading, modelPrices, nowMs, sparklines }: StatCardsProps) { const { t } = useTranslation(); - const tokenBreakdown = usage ? calculateTokenBreakdown(usage) : { cachedTokens: 0, reasoningTokens: 0 }; - const rateStats = usage - ? calculateRecentPerMinuteRates(30, usage) - : { rpm: 0, tpm: 0, windowMinutes: 30, requestCount: 0, tokenCount: 0 }; - const totalCost = usage ? calculateTotalCost(usage, modelPrices) : 0; const hasPrices = Object.keys(modelPrices).length > 0; + const { tokenBreakdown, rateStats, totalCost } = useMemo(() => { + const empty = { + tokenBreakdown: { cachedTokens: 0, reasoningTokens: 0 }, + rateStats: { rpm: 0, tpm: 0, windowMinutes: 30, requestCount: 0, tokenCount: 0 }, + totalCost: 0 + }; + + if (!usage) return empty; + const details = collectUsageDetails(usage); + if (!details.length) return empty; + + let cachedTokens = 0; + let reasoningTokens = 0; + let totalCost = 0; + + const now = nowMs; + const windowMinutes = 30; + const windowStart = now - windowMinutes * 60 * 1000; + let requestCount = 0; + let tokenCount = 0; + const hasValidNow = Number.isFinite(now) && now > 0; + + details.forEach((detail) => { + const tokens = detail.tokens; + cachedTokens += Math.max( + typeof tokens.cached_tokens === 'number' ? Math.max(tokens.cached_tokens, 0) : 0, + typeof tokens.cache_tokens === 'number' ? Math.max(tokens.cache_tokens, 0) : 0 + ); + if (typeof tokens.reasoning_tokens === 'number') { + reasoningTokens += tokens.reasoning_tokens; + } + + const timestamp = detail.__timestampMs ?? 0; + if (hasValidNow && Number.isFinite(timestamp) && timestamp >= windowStart && timestamp <= now) { + requestCount += 1; + tokenCount += extractTotalTokens(detail); + } + + if (hasPrices) { + totalCost += calculateCost(detail, modelPrices); + } + }); + + const denominator = windowMinutes > 0 ? windowMinutes : 1; + return { + tokenBreakdown: { cachedTokens, reasoningTokens }, + rateStats: { + rpm: requestCount / denominator, + tpm: tokenCount / denominator, + windowMinutes, + requestCount, + tokenCount + }, + totalCost + }; + }, [hasPrices, modelPrices, nowMs, usage]); + const statsCards: StatCardData[] = [ { key: 'requests', diff --git a/src/components/usage/hooks/useSparklines.ts b/src/components/usage/hooks/useSparklines.ts index 5b6e6ad7..0572682c 100644 --- a/src/components/usage/hooks/useSparklines.ts +++ b/src/components/usage/hooks/useSparklines.ts @@ -24,6 +24,7 @@ export interface SparklineBundle { export interface UseSparklinesOptions { usage: UsagePayload | null; loading: boolean; + nowMs: number; } export interface UseSparklinesReturn { @@ -34,42 +35,43 @@ export interface UseSparklinesReturn { costSparkline: SparklineBundle | null; } -export function useSparklines({ usage, loading }: UseSparklinesOptions): UseSparklinesReturn { - const buildLastHourSeries = useCallback( - (metric: 'requests' | 'tokens'): { labels: string[]; data: number[] } => { - if (!usage) return { labels: [], data: [] }; - const details = collectUsageDetails(usage); - if (!details.length) return { labels: [], data: [] }; +export function useSparklines({ usage, loading, nowMs }: UseSparklinesOptions): UseSparklinesReturn { + const lastHourSeries = useMemo(() => { + if (!usage) return { labels: [], requests: [], tokens: [] }; + if (!Number.isFinite(nowMs) || nowMs <= 0) { + return { labels: [], requests: [], tokens: [] }; + } + const details = collectUsageDetails(usage); + if (!details.length) return { labels: [], requests: [], tokens: [] }; - const windowMinutes = 60; - const now = Date.now(); - const windowStart = now - windowMinutes * 60 * 1000; - const buckets = new Array(windowMinutes).fill(0); + const windowMinutes = 60; + const now = nowMs; + const windowStart = now - windowMinutes * 60 * 1000; + const requestBuckets = new Array(windowMinutes).fill(0); + const tokenBuckets = new Array(windowMinutes).fill(0); - details.forEach((detail) => { - const timestamp = Date.parse(detail.timestamp); - if (Number.isNaN(timestamp) || timestamp < windowStart) { - return; - } - const minuteIndex = Math.min( - windowMinutes - 1, - Math.floor((timestamp - windowStart) / 60000) - ); - const increment = metric === 'tokens' ? extractTotalTokens(detail) : 1; - buckets[minuteIndex] += increment; - }); + details.forEach((detail) => { + const timestamp = detail.__timestampMs ?? 0; + if (!Number.isFinite(timestamp) || timestamp < windowStart || timestamp > now) { + return; + } + const minuteIndex = Math.min( + windowMinutes - 1, + Math.floor((timestamp - windowStart) / 60000) + ); + requestBuckets[minuteIndex] += 1; + tokenBuckets[minuteIndex] += extractTotalTokens(detail); + }); - const labels = buckets.map((_, idx) => { - const date = new Date(windowStart + (idx + 1) * 60000); - const h = date.getHours().toString().padStart(2, '0'); - const m = date.getMinutes().toString().padStart(2, '0'); - return `${h}:${m}`; - }); + const labels = requestBuckets.map((_, idx) => { + const date = new Date(windowStart + (idx + 1) * 60000); + const h = date.getHours().toString().padStart(2, '0'); + const m = date.getMinutes().toString().padStart(2, '0'); + return `${h}:${m}`; + }); - return { labels, data: buckets }; - }, - [usage] - ); + return { labels, requests: requestBuckets, tokens: tokenBuckets }; + }, [nowMs, usage]); const buildSparkline = useCallback( ( @@ -104,28 +106,53 @@ export function useSparklines({ usage, loading }: UseSparklinesOptions): UseSpar ); const requestsSparkline = useMemo( - () => buildSparkline(buildLastHourSeries('requests'), '#8b8680', 'rgba(139, 134, 128, 0.18)'), - [buildLastHourSeries, buildSparkline] + () => + buildSparkline( + { labels: lastHourSeries.labels, data: lastHourSeries.requests }, + '#8b8680', + 'rgba(139, 134, 128, 0.18)' + ), + [buildSparkline, lastHourSeries.labels, lastHourSeries.requests] ); const tokensSparkline = useMemo( - () => buildSparkline(buildLastHourSeries('tokens'), '#8b5cf6', 'rgba(139, 92, 246, 0.18)'), - [buildLastHourSeries, buildSparkline] + () => + buildSparkline( + { labels: lastHourSeries.labels, data: lastHourSeries.tokens }, + '#8b5cf6', + 'rgba(139, 92, 246, 0.18)' + ), + [buildSparkline, lastHourSeries.labels, lastHourSeries.tokens] ); const rpmSparkline = useMemo( - () => buildSparkline(buildLastHourSeries('requests'), '#22c55e', 'rgba(34, 197, 94, 0.18)'), - [buildLastHourSeries, buildSparkline] + () => + buildSparkline( + { labels: lastHourSeries.labels, data: lastHourSeries.requests }, + '#22c55e', + 'rgba(34, 197, 94, 0.18)' + ), + [buildSparkline, lastHourSeries.labels, lastHourSeries.requests] ); const tpmSparkline = useMemo( - () => buildSparkline(buildLastHourSeries('tokens'), '#f97316', 'rgba(249, 115, 22, 0.18)'), - [buildLastHourSeries, buildSparkline] + () => + buildSparkline( + { labels: lastHourSeries.labels, data: lastHourSeries.tokens }, + '#f97316', + 'rgba(249, 115, 22, 0.18)' + ), + [buildSparkline, lastHourSeries.labels, lastHourSeries.tokens] ); const costSparkline = useMemo( - () => buildSparkline(buildLastHourSeries('tokens'), '#f59e0b', 'rgba(245, 158, 11, 0.18)'), - [buildLastHourSeries, buildSparkline] + () => + buildSparkline( + { labels: lastHourSeries.labels, data: lastHourSeries.tokens }, + '#f59e0b', + 'rgba(245, 158, 11, 0.18)' + ), + [buildSparkline, lastHourSeries.labels, lastHourSeries.tokens] ); return { diff --git a/src/pages/UsagePage.tsx b/src/pages/UsagePage.tsx index 9339acb0..80034c61 100644 --- a/src/pages/UsagePage.tsx +++ b/src/pages/UsagePage.tsx @@ -187,6 +187,8 @@ export function UsagePage() { } }, [timeRange]); + const nowMs = lastRefreshedAt?.getTime() ?? 0; + // Sparklines hook const { requestsSparkline, @@ -194,7 +196,7 @@ export function UsagePage() { rpmSparkline, tpmSparkline, costSparkline - } = useSparklines({ usage: filteredUsage, loading }); + } = useSparklines({ usage: filteredUsage, loading, nowMs }); // Chart data hook const { @@ -293,6 +295,7 @@ export function UsagePage() { usage={filteredUsage} loading={loading} modelPrices={modelPrices} + nowMs={nowMs} sparklines={{ requests: requestsSparkline, tokens: tokensSparkline, diff --git a/src/stores/useUsageStatsStore.ts b/src/stores/useUsageStatsStore.ts index 20d5ac50..e431f586 100644 --- a/src/stores/useUsageStatsStore.ts +++ b/src/stores/useUsageStatsStore.ts @@ -1,7 +1,7 @@ import { create } from 'zustand'; import { usageApi } from '@/services/api'; import { useAuthStore } from '@/stores/useAuthStore'; -import { collectUsageDetails, computeKeyStats, type KeyStats, type UsageDetail } from '@/utils/usage'; +import { collectUsageDetails, computeKeyStatsFromDetails, type KeyStats, type UsageDetail } from '@/utils/usage'; import i18n from '@/i18n'; export const USAGE_STATS_STALE_TIME_MS = 240_000; @@ -98,10 +98,11 @@ export const useUsageStatsStore = create((set, get) => ({ if (requestId !== usageRequestToken) return; + const usageDetails = collectUsageDetails(usage); set({ usage, - keyStats: computeKeyStats(usage), - usageDetails: collectUsageDetails(usage), + keyStats: computeKeyStatsFromDetails(usageDetails), + usageDetails, loading: false, error: null, lastRefreshedAt: Date.now(), diff --git a/src/utils/usage.ts b/src/utils/usage.ts index 2d5f3781..07abb00f 100644 --- a/src/utils/usage.ts +++ b/src/utils/usage.ts @@ -49,6 +49,7 @@ export interface UsageDetail { }; failed: boolean; __modelName?: string; + __timestampMs?: number; } export interface UsageDetailWithEndpoint extends UsageDetail { @@ -109,34 +110,6 @@ const toUsageSummaryFields = (summary: UsageSummary) => ({ total_tokens: summary.totalTokens }); -const isDetailWithinWindow = (detail: unknown, windowStart: number, nowMs: number): detail is Record => { - if (!isRecord(detail) || typeof detail.timestamp !== 'string') { - return false; - } - const timestamp = Date.parse(detail.timestamp); - if (Number.isNaN(timestamp)) { - return false; - } - return timestamp >= windowStart && timestamp <= nowMs; -}; - -const updateSummaryFromDetails = (summary: UsageSummary, details: unknown[]) => { - details.forEach((detail) => { - const detailRecord = isRecord(detail) ? detail : null; - if (!detailRecord) { - return; - } - - summary.totalRequests += 1; - if (detailRecord.failed === true) { - summary.failureCount += 1; - } else { - summary.successCount += 1; - } - summary.totalTokens += extractTotalTokens(detailRecord); - }); -}; - export function filterUsageByTimeRange(usageData: T, range: UsageTimeRange, nowMs: number = Date.now()): T { if (range === 'all') { return usageData; @@ -169,6 +142,7 @@ export function filterUsageByTimeRange(usageData: T, range: UsageTimeRange, n const filteredModels: Record = {}; const apiSummary = createUsageSummary(); + let hasModelData = false; Object.entries(models).forEach(([modelName, modelEntry]) => { if (!isRecord(modelEntry)) { @@ -176,22 +150,39 @@ export function filterUsageByTimeRange(usageData: T, range: UsageTimeRange, n } const detailsRaw = Array.isArray(modelEntry.details) ? modelEntry.details : []; - const filteredDetails = detailsRaw.filter((detail) => - isDetailWithinWindow(detail, windowStart, nowMs) - ); + const modelSummary = createUsageSummary(); + const filteredDetails: unknown[] = []; + + detailsRaw.forEach((detail) => { + const detailRecord = isRecord(detail) ? detail : null; + if (!detailRecord || typeof detailRecord.timestamp !== 'string') { + return; + } + const timestamp = Date.parse(detailRecord.timestamp); + if (Number.isNaN(timestamp) || timestamp < windowStart || timestamp > nowMs) { + return; + } + + filteredDetails.push(detail); + modelSummary.totalRequests += 1; + if (detailRecord.failed === true) { + modelSummary.failureCount += 1; + } else { + modelSummary.successCount += 1; + } + modelSummary.totalTokens += extractTotalTokens(detailRecord); + }); if (!filteredDetails.length) { return; } - const modelSummary = createUsageSummary(); - updateSummaryFromDetails(modelSummary, filteredDetails); - filteredModels[modelName] = { ...modelEntry, ...toUsageSummaryFields(modelSummary), details: filteredDetails }; + hasModelData = true; apiSummary.totalRequests += modelSummary.totalRequests; apiSummary.successCount += modelSummary.successCount; @@ -199,7 +190,7 @@ export function filterUsageByTimeRange(usageData: T, range: UsageTimeRange, n apiSummary.totalTokens += modelSummary.totalTokens; }); - if (Object.keys(filteredModels).length === 0) { + if (!hasModelData) { return; } @@ -450,13 +441,40 @@ export function formatUsd(value: number): string { return `$${parts}`; } +const usageDetailsCache = new WeakMap(); +const usageDetailsWithEndpointCache = new WeakMap(); + /** * 从使用数据中收集所有请求明细 */ export function collectUsageDetails(usageData: unknown): UsageDetail[] { + const cacheKey = isRecord(usageData) ? (usageData as object) : null; + if (cacheKey) { + const cached = usageDetailsCache.get(cacheKey); + if (cached) return cached; + } + const apis = getApisRecord(usageData); if (!apis) return []; const details: UsageDetail[] = []; + const sourceCache = new Map(); + + const normalizeSource = (value: unknown): string => { + const raw = + typeof value === 'string' + ? value + : value === null || value === undefined + ? '' + : String(value); + const trimmed = raw.trim(); + if (!trimmed) return ''; + const cached = sourceCache.get(trimmed); + if (cached !== undefined) return cached; + const normalized = normalizeUsageSourceId(trimmed); + sourceCache.set(trimmed, normalized); + return normalized; + }; + Object.values(apis).forEach((apiEntry) => { if (!isRecord(apiEntry)) return; const modelsRaw = apiEntry.models; @@ -470,15 +488,25 @@ export function collectUsageDetails(usageData: unknown): UsageDetail[] { modelDetails.forEach((detailRaw) => { if (!isRecord(detailRaw) || typeof detailRaw.timestamp !== 'string') return; - const detail = detailRaw as unknown as UsageDetail; + const timestamp = detailRaw.timestamp; + const timestampMs = Date.parse(timestamp); + const tokensRaw = isRecord(detailRaw.tokens) ? detailRaw.tokens : {}; details.push({ - ...detail, - source: normalizeUsageSourceId(detail.source), + timestamp, + source: normalizeSource(detailRaw.source), + auth_index: detailRaw.auth_index as unknown as number, + tokens: tokensRaw as unknown as UsageDetail['tokens'], + failed: detailRaw.failed === true, __modelName: modelName, + __timestampMs: Number.isNaN(timestampMs) ? 0 : timestampMs, }); }); }); }); + + if (cacheKey) { + usageDetailsCache.set(cacheKey, details); + } return details; } @@ -486,10 +514,34 @@ export function collectUsageDetails(usageData: unknown): UsageDetail[] { * 从使用数据中收集包含 endpoint/method/path 的请求明细 */ export function collectUsageDetailsWithEndpoint(usageData: unknown): UsageDetailWithEndpoint[] { + const cacheKey = isRecord(usageData) ? (usageData as object) : null; + if (cacheKey) { + const cached = usageDetailsWithEndpointCache.get(cacheKey); + if (cached) return cached; + } + const apis = getApisRecord(usageData); if (!apis) return []; const details: UsageDetailWithEndpoint[] = []; + const sourceCache = new Map(); + + const normalizeSource = (value: unknown): string => { + const raw = + typeof value === 'string' + ? value + : value === null || value === undefined + ? '' + : String(value); + const trimmed = raw.trim(); + if (!trimmed) return ''; + const cached = sourceCache.get(trimmed); + if (cached !== undefined) return cached; + const normalized = normalizeUsageSourceId(trimmed); + sourceCache.set(trimmed, normalized); + return normalized; + }; + Object.entries(apis).forEach(([endpoint, apiEntry]) => { if (!isRecord(apiEntry)) return; const modelsRaw = apiEntry.models; @@ -507,11 +559,15 @@ export function collectUsageDetailsWithEndpoint(usageData: unknown): UsageDetail modelDetails.forEach((detailRaw) => { if (!isRecord(detailRaw) || typeof detailRaw.timestamp !== 'string') return; - const detail = detailRaw as unknown as UsageDetail; - const timestampMs = Date.parse(detail.timestamp); + const timestamp = detailRaw.timestamp; + const timestampMs = Date.parse(timestamp); + const tokensRaw = isRecord(detailRaw.tokens) ? detailRaw.tokens : {}; details.push({ - ...detail, - source: normalizeUsageSourceId(detail.source), + timestamp, + source: normalizeSource(detailRaw.source), + auth_index: detailRaw.auth_index as unknown as number, + tokens: tokensRaw as unknown as UsageDetail['tokens'], + failed: detailRaw.failed === true, __modelName: modelName, __endpoint: endpoint, __endpointMethod: endpointMethod, @@ -522,6 +578,9 @@ export function collectUsageDetailsWithEndpoint(usageData: unknown): UsageDetail }); }); + if (cacheKey) { + usageDetailsWithEndpointCache.set(cacheKey, details); + } return details; } @@ -592,8 +651,9 @@ export function calculateRecentPerMinuteRates( let tokenCount = 0; details.forEach((detail) => { - const timestamp = Date.parse(detail.timestamp); - if (Number.isNaN(timestamp) || timestamp < windowStart) { + const timestamp = + typeof detail.__timestampMs === 'number' ? detail.__timestampMs : Date.parse(detail.timestamp); + if (!Number.isFinite(timestamp) || timestamp < windowStart || timestamp > now) { return; } requestCount += 1; @@ -951,8 +1011,9 @@ export function buildHourlySeriesByModel( } details.forEach((detail) => { - const timestamp = Date.parse(detail.timestamp); - if (Number.isNaN(timestamp)) { + const timestamp = + typeof detail.__timestampMs === 'number' ? detail.__timestampMs : Date.parse(detail.timestamp); + if (!Number.isFinite(timestamp) || timestamp <= 0) { return; } @@ -1007,8 +1068,9 @@ export function buildDailySeriesByModel( } details.forEach((detail) => { - const timestamp = Date.parse(detail.timestamp); - if (Number.isNaN(timestamp)) { + const timestamp = + typeof detail.__timestampMs === 'number' ? detail.__timestampMs : Date.parse(detail.timestamp); + if (!Number.isFinite(timestamp) || timestamp <= 0) { return; } const dayLabel = formatDayLabel(new Date(timestamp)); @@ -1220,8 +1282,9 @@ export function calculateStatusBarData( // Filter and bucket the usage details usageDetails.forEach((detail) => { - const timestamp = Date.parse(detail.timestamp); - if (Number.isNaN(timestamp) || timestamp < windowStart || timestamp > now) { + const timestamp = + typeof detail.__timestampMs === 'number' ? detail.__timestampMs : Date.parse(detail.timestamp); + if (!Number.isFinite(timestamp) || timestamp <= 0 || timestamp < windowStart || timestamp > now) { return; } @@ -1322,8 +1385,9 @@ export function calculateServiceHealthData( let totalFailure = 0; usageDetails.forEach((detail) => { - const timestamp = Date.parse(detail.timestamp); - if (Number.isNaN(timestamp) || timestamp < windowStart || timestamp > now) { + const timestamp = + typeof detail.__timestampMs === 'number' ? detail.__timestampMs : Date.parse(detail.timestamp); + if (!Number.isFinite(timestamp) || timestamp <= 0 || timestamp < windowStart || timestamp > now) { return; } @@ -1439,6 +1503,44 @@ export function computeKeyStats(usageData: unknown, masker: (val: string) => str }; } +export function computeKeyStatsFromDetails(usageDetails: UsageDetail[]): KeyStats { + const bySource: Record = {}; + const byAuthIndex: Record = {}; + + const ensureBucket = (bucket: Record, key: string) => { + if (!bucket[key]) { + bucket[key] = { success: 0, failure: 0 }; + } + return bucket[key]; + }; + + usageDetails.forEach((detail) => { + const source = detail.source; + const authIndexKey = normalizeAuthIndex(detail.auth_index); + const isFailed = detail.failed === true; + + if (source) { + const bucket = ensureBucket(bySource, source); + if (isFailed) { + bucket.failure += 1; + } else { + bucket.success += 1; + } + } + + if (authIndexKey) { + const bucket = ensureBucket(byAuthIndex, authIndexKey); + if (isFailed) { + bucket.failure += 1; + } else { + bucket.success += 1; + } + } + }); + + return { bySource, byAuthIndex }; +} + export type TokenCategory = 'input' | 'output' | 'cached' | 'reasoning'; export interface TokenBreakdownSeries { @@ -1483,8 +1585,9 @@ export function buildHourlyTokenBreakdown( let hasData = false; details.forEach((detail) => { - const timestamp = Date.parse(detail.timestamp); - if (Number.isNaN(timestamp)) return; + const timestamp = + typeof detail.__timestampMs === 'number' ? detail.__timestampMs : Date.parse(detail.timestamp); + if (!Number.isFinite(timestamp) || timestamp <= 0) return; const normalized = new Date(timestamp); normalized.setMinutes(0, 0, 0); const bucketStart = normalized.getTime(); @@ -1521,8 +1624,9 @@ export function buildDailyTokenBreakdown(usageData: unknown): TokenBreakdownSeri let hasData = false; details.forEach((detail) => { - const timestamp = Date.parse(detail.timestamp); - if (Number.isNaN(timestamp)) return; + const timestamp = + typeof detail.__timestampMs === 'number' ? detail.__timestampMs : Date.parse(detail.timestamp); + if (!Number.isFinite(timestamp) || timestamp <= 0) return; const dayLabel = formatDayLabel(new Date(timestamp)); if (!dayLabel) return; @@ -1594,8 +1698,9 @@ export function buildHourlyCostSeries( let hasData = false; details.forEach((detail) => { - const timestamp = Date.parse(detail.timestamp); - if (Number.isNaN(timestamp)) return; + const timestamp = + typeof detail.__timestampMs === 'number' ? detail.__timestampMs : Date.parse(detail.timestamp); + if (!Number.isFinite(timestamp) || timestamp <= 0) return; const normalized = new Date(timestamp); normalized.setMinutes(0, 0, 0); const bucketStart = normalized.getTime(); @@ -1626,8 +1731,9 @@ export function buildDailyCostSeries( let hasData = false; details.forEach((detail) => { - const timestamp = Date.parse(detail.timestamp); - if (Number.isNaN(timestamp)) return; + const timestamp = + typeof detail.__timestampMs === 'number' ? detail.__timestampMs : Date.parse(detail.timestamp); + if (!Number.isFinite(timestamp) || timestamp <= 0) return; const dayLabel = formatDayLabel(new Date(timestamp)); if (!dayLabel) return;