import { Fragment, type ReactNode } from 'react'; import { Zap } from 'lucide-react'; import { Tooltip, TooltipContent, TooltipTrigger } from './ui/Tooltip'; import { defineMessages, useIntl } from '../i18n'; import { cn } from '../utils'; import type { MessageUsage } from '../types/message'; import { formatCost, formatDuration, formatTokenCount, formatTokensPerSecond, tokensPerSecond, } from '../utils/usageFormatting'; const i18n = defineMessages({ tokensPerSecondUnit: { id: 'messageUsageStats.tokensPerSecondUnit', defaultMessage: 'tok/s', }, tokenUnit: { id: 'messageUsageStats.tokenUnit', defaultMessage: 'tok', }, input: { id: 'messageUsageStats.input', defaultMessage: 'Input', }, cacheRead: { id: 'messageUsageStats.cacheRead', defaultMessage: 'cache read', }, cacheWrite: { id: 'messageUsageStats.cacheWrite', defaultMessage: 'cache write', }, cacheHitRate: { id: 'messageUsageStats.cacheHitRate', defaultMessage: '({percent}% of input)', }, output: { id: 'messageUsageStats.output', defaultMessage: 'Output', }, total: { id: 'messageUsageStats.total', defaultMessage: 'Total', }, firstToken: { id: 'messageUsageStats.firstToken', defaultMessage: 'First token', }, totalTime: { id: 'messageUsageStats.totalTime', defaultMessage: 'Total time', }, speed: { id: 'messageUsageStats.speed', defaultMessage: 'Speed', }, cost: { id: 'messageUsageStats.cost', defaultMessage: 'Cost', }, estimated: { id: 'messageUsageStats.estimated', defaultMessage: '(estimated)', }, reported: { id: 'messageUsageStats.reported', defaultMessage: '(reported)', }, compaction: { id: 'messageUsageStats.compaction', defaultMessage: 'Compaction', }, }); function hasValue(value: number | null | undefined): value is number { return typeof value === 'number' && Number.isFinite(value); } /** One label/value row of the tooltip's stats card. */ function StatRow({ label, value, sub, }: { label: string; value: ReactNode; /** Dimmer, indented breakdown row (e.g. cache read/write). */ sub?: boolean; }) { return (
{label} {value}
); } /** * Per-message usage chip (tok/s, cost, total tokens) with a tooltip breaking * down tokens, caching, timing, and cost. Renders nothing without data. */ export default function MessageUsageStats({ usage }: { usage: MessageUsage }) { const intl = useIntl(); const { inputTokens, outputTokens, totalTokens, cacheReadTokens, cacheWriteTokens, cost, costSource, elapsedMs, timeToFirstTokenMs, isCompaction, } = usage; const tps = tokensPerSecond(outputTokens, elapsedMs); const chipSegments: ReactNode[] = []; if (tps !== null) { chipSegments.push( ); } if (hasValue(cost)) { chipSegments.push( {costSource === 'estimated' ? '~' : ''} {formatCost(cost)} ); } if (hasValue(totalTokens)) { chipSegments.push( {formatTokenCount(totalTokens)} {intl.formatMessage(i18n.tokenUnit)} ); } if (chipSegments.length === 0) { return null; } const hasTokensSection = hasValue(inputTokens) || hasValue(outputTokens) || hasValue(totalTokens) || hasValue(cacheReadTokens) || hasValue(cacheWriteTokens); const hasTimingSection = hasValue(timeToFirstTokenMs) || hasValue(elapsedMs) || tps !== null; const hasCostSection = hasValue(cost); const section = 'py-1.5 first:pt-0 last:pb-0 space-y-[3px]'; return (
{chipSegments.map((segment, index) => ( {index > 0 && ยท} {segment} ))}
{hasTokensSection && (
{hasValue(inputTokens) && ( )} {hasValue(cacheReadTokens) && ( {formatTokenCount(cacheReadTokens)} {hasValue(inputTokens) && inputTokens > 0 && ( {' '} {intl.formatMessage(i18n.cacheHitRate, { percent: Math.round((cacheReadTokens / inputTokens) * 100), })} )} } /> )} {hasValue(cacheWriteTokens) && ( )} {hasValue(outputTokens) && ( )} {hasValue(totalTokens) && ( )}
)} {hasTimingSection && (
{hasValue(timeToFirstTokenMs) && ( )} {hasValue(elapsedMs) && ( )} {tps !== null && ( )}
)} {hasCostSection && (
{hasValue(cost) && ( {formatCost(cost)} {(costSource === 'provider_reported' || costSource === 'estimated') && ( {' '} {intl.formatMessage( costSource === 'provider_reported' ? i18n.reported : i18n.estimated )} )} } /> )}
)} {isCompaction && (
{intl.formatMessage(i18n.compaction)}
)}
); }