/** * @license * Copyright 2025 Vybestack LLC * SPDX-License-Identifier: Apache-2.0 */ import type React from 'react'; import { Box, Text } from 'ink'; import { theme } from '../semantic-colors.js'; import { formatDuration } from '../utils/formatters.js'; import { calculateAverageLatency, calculateCachedTokenRatio, calculateErrorRate, } from '../utils/computeStats.js'; import { useSessionStats, type SessionMetrics, } from '../contexts/SessionContext.js'; import { Table, type Column } from './Table.js'; import { getBorderStyle } from '../contexts/UnicodeRenderingContext.js'; interface StatRowData { metric: string; isSection?: boolean; isSubtle?: boolean; [key: string]: string | React.ReactNode | boolean | undefined; } type ModelMetrics = SessionMetrics['models'][string]; type ActiveModelEntry = [string, ModelMetrics]; function createStatRow( activeModels: ActiveModelEntry[], metric: string, getValue: (metrics: ModelMetrics) => string | React.ReactNode, options: { isSection?: boolean; isSubtle?: boolean } = {}, ): StatRowData { const row: StatRowData = { metric, isSection: options.isSection, isSubtle: options.isSubtle, }; activeModels.forEach(([name, metrics]) => { row[name] = getValue(metrics); }); return row; } function buildApiSectionRows(activeModels: ActiveModelEntry[]): StatRowData[] { return [ { metric: 'API', isSection: true }, createStatRow(activeModels, 'Requests', (m) => m.api.totalRequests.toLocaleString(), ), createStatRow(activeModels, 'Errors', (m) => { const errorRate = calculateErrorRate(m); return ( 0 ? theme.status.error : theme.text.primary } > {m.api.totalErrors.toLocaleString()} ({errorRate.toFixed(1)}%) ); }), createStatRow(activeModels, 'Avg Latency', (m) => formatDuration(calculateAverageLatency(m)), ), { metric: '' }, { metric: 'Tokens', isSection: true }, ]; } function buildBaseTokenRows(activeModels: ActiveModelEntry[]): StatRowData[] { return [ createStatRow(activeModels, 'Total', (m) => ( {m.tokens.total.toLocaleString()} )), createStatRow( activeModels, 'Input', (m) => ( {m.tokens.input.toLocaleString()} ), { isSubtle: true }, ), ]; } function buildOptionalTokenRows( activeModels: ActiveModelEntry[], hasCached: boolean, hasThoughts: boolean, hasTool: boolean, ): StatRowData[] { const rows: StatRowData[] = []; if (hasCached) { rows.push( createStatRow( activeModels, 'Cache Reads', (m) => { const cachedTokenRatio = calculateCachedTokenRatio(m); return ( {m.tokens.cached.toLocaleString()} ({cachedTokenRatio.toFixed(1)} %) ); }, { isSubtle: true }, ), ); } if (hasThoughts) { rows.push( createStatRow( activeModels, 'Thoughts', (m) => ( {m.tokens.thoughts.toLocaleString()} ), { isSubtle: true }, ), ); } if (hasTool) { rows.push( createStatRow( activeModels, 'Tool', (m) => ( {m.tokens.tool.toLocaleString()} ), { isSubtle: true }, ), ); } return rows; } function buildTokenRows( activeModels: ActiveModelEntry[], hasCached: boolean, hasThoughts: boolean, hasTool: boolean, ): StatRowData[] { const rows = buildBaseTokenRows(activeModels); rows.push( ...buildOptionalTokenRows(activeModels, hasCached, hasThoughts, hasTool), ); rows.push( createStatRow( activeModels, 'Output', (m) => ( {m.tokens.candidates.toLocaleString()} ), { isSubtle: true }, ), ); return rows; } function buildColumns(modelNames: string[]): Array> { return [ { key: 'metric', header: 'Metric', width: 28, renderCell: (row) => ( {row.isSubtle === true ? ` ↳ ${row.metric}` : row.metric} ), }, ...modelNames.map((name) => ({ key: name, header: name, flexGrow: 1, renderCell: (row: StatRowData) => { if (row.isSection === true) return null; const val = row[name]; if (val === undefined || val === null) return null; if (typeof val === 'string' || typeof val === 'number') { return {val}; } return val as React.ReactNode; }, })), ]; } export const ModelStatsDisplay: React.FC = () => { const { stats } = useSessionStats(); const { models } = stats.metrics; const activeModels = Object.entries(models).filter( ([, metrics]) => metrics.api.totalRequests > 0, ) as ActiveModelEntry[]; if (activeModels.length === 0) { return ( No API calls have been made in this session. ); } const modelNames = activeModels.map(([name]) => name); const hasThoughts = activeModels.some( ([, metrics]) => metrics.tokens.thoughts > 0, ); const hasTool = activeModels.some(([, metrics]) => metrics.tokens.tool > 0); const hasCached = activeModels.some( ([, metrics]) => metrics.tokens.cached > 0, ); const rows = [ ...buildApiSectionRows(activeModels), ...buildTokenRows(activeModels, hasCached, hasThoughts, hasTool), ]; const columns = buildColumns(modelNames); return ( Model Stats For Nerds ); };