feat: improve usage latency formatting

This commit is contained in:
Michael
2026-04-05 11:14:46 +08:00
parent 13a0283e00
commit d84cf61915
8 changed files with 242 additions and 52 deletions
+49 -11
View File
@@ -1,17 +1,15 @@
import { useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Card } from '@/components/ui/Card';
import { formatCompactNumber, formatUsd } from '@/utils/usage';
import {
formatCompactNumber,
formatDurationMs,
formatUsd,
type ModelStatsSummary,
} from '@/utils/usage';
import styles from '@/pages/UsagePage.module.scss';
export interface ModelStat {
model: string;
requests: number;
successCount: number;
failureCount: number;
tokens: number;
cost: number;
}
export type ModelStat = ModelStatsSummary;
export interface ModelStatsCardProps {
modelStats: ModelStat[];
@@ -19,7 +17,14 @@ export interface ModelStatsCardProps {
hasPrices: boolean;
}
type SortKey = 'model' | 'requests' | 'tokens' | 'cost' | 'successRate';
type SortKey =
| 'model'
| 'requests'
| 'tokens'
| 'cost'
| 'successRate'
| 'averageLatencyMs'
| 'totalLatencyMs';
type SortDir = 'asc' | 'desc';
interface ModelStatWithRate extends ModelStat {
@@ -48,7 +53,15 @@ export function ModelStatsCard({ modelStats, loading, hasPrices }: ModelStatsCar
const dir = sortDir === 'asc' ? 1 : -1;
list.sort((a, b) => {
if (sortKey === 'model') return dir * a.model.localeCompare(b.model);
return dir * ((a[sortKey] as number) - (b[sortKey] as number));
const left = a[sortKey];
const right = b[sortKey];
const leftValid = typeof left === 'number' && Number.isFinite(left);
const rightValid = typeof right === 'number' && Number.isFinite(right);
if (!leftValid && !rightValid) return 0;
if (!leftValid) return 1;
if (!rightValid) return -1;
return dir * (left - right);
});
return list;
}, [modelStats, sortKey, sortDir]);
@@ -95,6 +108,27 @@ export function ModelStatsCard({ modelStats, loading, hasPrices }: ModelStatsCar
{t('usage_stats.tokens_count')}{arrow('tokens')}
</button>
</th>
<th
className={styles.sortableHeader}
aria-sort={ariaSort('averageLatencyMs')}
>
<button
type="button"
className={styles.sortHeaderButton}
onClick={() => handleSort('averageLatencyMs')}
>
{t('usage_stats.avg_time')}{arrow('averageLatencyMs')}
</button>
</th>
<th className={styles.sortableHeader} aria-sort={ariaSort('totalLatencyMs')}>
<button
type="button"
className={styles.sortHeaderButton}
onClick={() => handleSort('totalLatencyMs')}
>
{t('usage_stats.total_time')}{arrow('totalLatencyMs')}
</button>
</th>
<th className={styles.sortableHeader} aria-sort={ariaSort('successRate')}>
<button
type="button"
@@ -131,6 +165,10 @@ export function ModelStatsCard({ modelStats, loading, hasPrices }: ModelStatsCar
</span>
</td>
<td>{formatCompactNumber(stat.tokens)}</td>
<td className={styles.durationCell}>
{formatDurationMs(stat.averageLatencyMs)}
</td>
<td className={styles.durationCell}>{formatDurationMs(stat.totalLatencyMs)}</td>
<td>
<span
className={
@@ -10,6 +10,7 @@ import type { AuthFileItem } from '@/types/authFile';
import type { CredentialInfo } from '@/types/sourceInfo';
import { buildSourceInfoMap, resolveSourceDisplay } from '@/utils/sourceResolver';
import {
extractLatencyMs,
collectUsageDetails,
extractTotalTokens,
formatDurationMs,
@@ -153,12 +154,7 @@ export function RequestEventsDetailsCard({
toNumber(detail.tokens?.total_tokens),
extractTotalTokens(detail)
);
const latencyMs =
typeof detail.latency_ms === 'number' &&
Number.isFinite(detail.latency_ms) &&
detail.latency_ms >= 0
? detail.latency_ms
: null;
const latencyMs = extractLatencyMs(detail);
return {
id: `${timestamp}-${model}-${sourceRaw || source}-${authIndex}-${index}`,
@@ -480,7 +476,9 @@ export function RequestEventsDetailsCard({
{row.failed ? t('stats.failure') : t('stats.success')}
</span>
</td>
{hasLatencyData && <td>{formatDurationMs(row.latencyMs)}</td>}
{hasLatencyData && (
<td className={styles.durationCell}>{formatDurationMs(row.latencyMs)}</td>
)}
<td>{row.inputTokens.toLocaleString()}</td>
<td>{row.outputTokens.toLocaleString()}</td>
<td>{row.reasoningTokens.toLocaleString()}</td>
+5 -15
View File
@@ -9,6 +9,7 @@ import {
IconTrendingUp,
} from '@/components/ui/icons';
import {
calculateLatencyStatsFromDetails,
formatCompactNumber,
formatDurationMs,
formatPerMinuteValue,
@@ -59,18 +60,18 @@ export function StatCards({ usage, loading, modelPrices, nowMs, sparklines }: St
tokenBreakdown: { cachedTokens: 0, reasoningTokens: 0 },
rateStats: { rpm: 0, tpm: 0, windowMinutes: 30, requestCount: 0, tokenCount: 0 },
totalCost: 0,
latencyStats: { averageMs: null as number | null, sampleCount: 0 },
latencyStats: { averageMs: null as number | null, totalMs: null as number | null, sampleCount: 0 },
};
if (!usage) return empty;
const details = collectUsageDetails(usage);
if (!details.length) return empty;
const latencyStats = calculateLatencyStatsFromDetails(details);
let cachedTokens = 0;
let reasoningTokens = 0;
let totalCost = 0;
let latencyTotalMs = 0;
let latencySampleCount = 0;
const now = nowMs;
const windowMinutes = 30;
@@ -88,14 +89,6 @@ export function StatCards({ usage, loading, modelPrices, nowMs, sparklines }: St
if (typeof tokens.reasoning_tokens === 'number') {
reasoningTokens += tokens.reasoning_tokens;
}
if (
typeof detail.latency_ms === 'number' &&
Number.isFinite(detail.latency_ms) &&
detail.latency_ms >= 0
) {
latencyTotalMs += detail.latency_ms;
latencySampleCount += 1;
}
const timestamp = detail.__timestampMs ?? 0;
if (
@@ -124,10 +117,7 @@ export function StatCards({ usage, loading, modelPrices, nowMs, sparklines }: St
tokenCount,
},
totalCost,
latencyStats: {
averageMs: latencySampleCount > 0 ? latencyTotalMs / latencySampleCount : null,
sampleCount: latencySampleCount,
},
latencyStats,
};
}, [hasPrices, modelPrices, nowMs, usage]);