fix(usage): stabilize dashboard metrics and reduce repeated parsing

This commit is contained in:
Supra4E8C
2026-02-22 18:48:01 +08:00
parent 3feb9c3a1f
commit 19532064da
7 changed files with 347 additions and 147 deletions
+36 -29
View File
@@ -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<string, CredentialBucket> = {};
const result: CredentialRow[] = [];
const consumedSourceIds = new Set<string>();
const authIndexToRowIndex = new Map<string, number>();
const sourceToAuthIndex = new Map<string, string>();
const sourceToAuthFile = new Map<string, CredentialInfo>();
const fallbackByAuthIndex = new Map<string, CredentialBucket>();
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<string, CredentialInfo>();
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;
@@ -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;
+63 -10
View File
@@ -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<string, ModelPrice>;
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',
+69 -42
View File
@@ -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 {
+4 -1
View File
@@ -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,
+4 -3
View File
@@ -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<UsageStatsState>((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(),
+166 -60
View File
@@ -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<string, unknown> => {
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<T>(usageData: T, range: UsageTimeRange, nowMs: number = Date.now()): T {
if (range === 'all') {
return usageData;
@@ -169,6 +142,7 @@ export function filterUsageByTimeRange<T>(usageData: T, range: UsageTimeRange, n
const filteredModels: Record<string, unknown> = {};
const apiSummary = createUsageSummary();
let hasModelData = false;
Object.entries(models).forEach(([modelName, modelEntry]) => {
if (!isRecord(modelEntry)) {
@@ -176,22 +150,39 @@ export function filterUsageByTimeRange<T>(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<T>(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<object, UsageDetail[]>();
const usageDetailsWithEndpointCache = new WeakMap<object, UsageDetailWithEndpoint[]>();
/**
* 从使用数据中收集所有请求明细
*/
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<string, string>();
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<string, string>();
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<string, KeyStatBucket> = {};
const byAuthIndex: Record<string, KeyStatBucket> = {};
const ensureBucket = (bucket: Record<string, KeyStatBucket>, 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;