mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-09-24 16:04:35 +08:00
feat: improve usage latency formatting
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user