Fix token usage handling and developer proxy save UX (#704)

This commit is contained in:
Lingxuan Zuo
2026-03-28 21:13:56 +08:00
committed by GitHub
Unverified
parent 2668082809
commit 870abb99c4
15 changed files with 782 additions and 75 deletions

View File

@@ -36,6 +36,37 @@ export function Models() {
const [usageWindow, setUsageWindow] = useState<UsageWindow>('7d');
const [usagePage, setUsagePage] = useState(1);
const [selectedUsageEntry, setSelectedUsageEntry] = useState<UsageHistoryEntry | null>(null);
const HIDDEN_USAGE_SOURCES = new Set([
'gateway-injected',
'delivery-mirror',
]);
function isHiddenUsageSource(source?: string): boolean {
if (!source) return false;
const normalizedSource = source.trim().toLowerCase();
return (
HIDDEN_USAGE_SOURCES.has(normalizedSource)
|| normalizedSource.includes('gateway-injected')
|| normalizedSource.includes('delivery-mirror')
);
}
function formatUsageSource(source?: string): string | undefined {
if (!source) return undefined;
if (isHiddenUsageSource(source)) {
return undefined;
}
return source;
}
function shouldHideUsageEntry(entry: UsageHistoryEntry): boolean {
return (
isHiddenUsageSource(entry.provider)
|| isHiddenUsageSource(entry.model)
);
}
type FetchState = {
status: 'idle' | 'loading' | 'done';
@@ -181,8 +212,9 @@ export function Models() {
};
}, [isGatewayRunning, gatewayStatus.connectedAt, gatewayStatus.pid, usageFetchMaxAttempts]);
const usageHistory = fetchState.data;
const visibleUsageHistory = isGatewayRunning ? usageHistory : [];
const visibleUsageHistory = isGatewayRunning
? fetchState.data.filter((entry) => !shouldHideUsageEntry(entry))
: [];
const filteredUsageHistory = filterUsageHistoryByWindow(visibleUsageHistory, usageWindow);
const usageGroups = groupUsageHistory(filteredUsageHistory, usageGroupBy);
const usagePageSize = 5;
@@ -313,6 +345,7 @@ export function Models() {
{pagedUsageHistory.map((entry) => (
<div
key={`${entry.sessionId}-${entry.timestamp}`}
data-testid="token-usage-entry"
className="rounded-2xl bg-transparent border border-black/10 dark:border-white/10 p-5 hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
>
<div className="flex items-start justify-between gap-3">
@@ -321,24 +354,46 @@ export function Models() {
{entry.model || t('dashboard:recentTokenHistory.unknownModel')}
</p>
<p className="text-[13px] text-muted-foreground truncate mt-0.5">
{[entry.provider, entry.agentId, entry.sessionId].filter(Boolean).join(' • ')}
{[formatUsageSource(entry.provider), formatUsageSource(entry.agentId), entry.sessionId].filter(Boolean).join(' • ')}
</p>
</div>
<div className="text-right shrink-0">
<p className="font-bold text-[15px]">{formatTokenCount(entry.totalTokens)}</p>
<p className={getUsageTotalClass(entry)}>
{formatUsageTotal(entry)}
</p>
{entry.usageStatus === 'missing' && (
<p className="text-[12px] text-muted-foreground mt-0.5">
{t('dashboard:recentTokenHistory.noUsage')}
</p>
)}
{entry.usageStatus === 'error' && (
<p className="text-[12px] text-red-500 dark:text-red-400 mt-0.5">
{t('dashboard:recentTokenHistory.usageParseError')}
</p>
)}
<p className="text-[12px] text-muted-foreground mt-0.5">
{formatUsageTimestamp(entry.timestamp)}
</p>
</div>
</div>
<div className="mt-3 flex flex-wrap gap-x-4 gap-y-1.5 text-[12.5px] font-medium text-muted-foreground">
<span className="flex items-center gap-1.5"><div className="w-2 h-2 rounded-full bg-sky-500"></div>{t('dashboard:recentTokenHistory.input', { value: formatTokenCount(entry.inputTokens) })}</span>
<span className="flex items-center gap-1.5"><div className="w-2 h-2 rounded-full bg-violet-500"></div>{t('dashboard:recentTokenHistory.output', { value: formatTokenCount(entry.outputTokens) })}</span>
{entry.cacheReadTokens > 0 && (
<span className="flex items-center gap-1.5"><div className="w-2 h-2 rounded-full bg-amber-500"></div>{t('dashboard:recentTokenHistory.cacheRead', { value: formatTokenCount(entry.cacheReadTokens) })}</span>
)}
{entry.cacheWriteTokens > 0 && (
<span className="flex items-center gap-1.5"><div className="w-2 h-2 rounded-full bg-amber-500"></div>{t('dashboard:recentTokenHistory.cacheWrite', { value: formatTokenCount(entry.cacheWriteTokens) })}</span>
{entry.usageStatus === 'available' || entry.usageStatus === undefined ? (
<>
<span className="flex items-center gap-1.5"><div className="w-2 h-2 rounded-full bg-sky-500"></div>{t('dashboard:recentTokenHistory.input', { value: formatTokenCount(entry.inputTokens) })}</span>
<span className="flex items-center gap-1.5"><div className="w-2 h-2 rounded-full bg-violet-500"></div>{t('dashboard:recentTokenHistory.output', { value: formatTokenCount(entry.outputTokens) })}</span>
{entry.cacheReadTokens > 0 && (
<span className="flex items-center gap-1.5"><div className="w-2 h-2 rounded-full bg-amber-500"></div>{t('dashboard:recentTokenHistory.cacheRead', { value: formatTokenCount(entry.cacheReadTokens) })}</span>
)}
{entry.cacheWriteTokens > 0 && (
<span className="flex items-center gap-1.5"><div className="w-2 h-2 rounded-full bg-amber-500"></div>{t('dashboard:recentTokenHistory.cacheWrite', { value: formatTokenCount(entry.cacheWriteTokens) })}</span>
)}
</>
) : (
<span className="text-[12px]">
{entry.usageStatus === 'missing'
? t('dashboard:recentTokenHistory.noUsage')
: t('dashboard:recentTokenHistory.usageParseError')}
</span>
)}
{typeof entry.costUsd === 'number' && Number.isFinite(entry.costUsd) && (
<span className="flex items-center gap-1.5 ml-auto text-foreground/80 bg-black/5 dark:bg-white/5 px-2 py-0.5 rounded-md">{t('dashboard:recentTokenHistory.cost', { amount: entry.costUsd.toFixed(4) })}</span>
@@ -409,6 +464,18 @@ function formatTokenCount(value: number): string {
return Intl.NumberFormat().format(value);
}
function getUsageTotalClass(entry: UsageHistoryEntry): string {
if (entry.usageStatus === 'error') return 'font-bold text-[15px] text-red-500 dark:text-red-400';
if (entry.usageStatus === 'missing') return 'font-bold text-[15px] text-muted-foreground';
return 'font-bold text-[15px]';
}
function formatUsageTotal(entry: UsageHistoryEntry): string {
if (entry.usageStatus === 'error') return '✕';
if (entry.usageStatus === 'missing') return '—';
return formatTokenCount(entry.totalTokens);
}
function formatUsageTimestamp(timestamp: string): string {
const date = new Date(timestamp);
if (Number.isNaN(date.getTime())) return timestamp;

View File

@@ -5,6 +5,7 @@ export type UsageHistoryEntry = {
model?: string;
provider?: string;
content?: string;
usageStatus?: 'available' | 'missing' | 'error';
inputTokens: number;
outputTokens: number;
cacheReadTokens: number;