Change detailed model stats to use a new shared Table class to resolve robustness issues. (#15208)

This commit is contained in:
Jacob Richman
2025-12-17 18:01:37 -08:00
committed by GitHub
parent 124a6da743
commit bc168bbae4
6 changed files with 415 additions and 184 deletions

View File

@@ -13,42 +13,17 @@ import {
calculateCacheHitRate,
calculateErrorRate,
} from '../utils/computeStats.js';
import type { ModelMetrics } from '../contexts/SessionContext.js';
import { useSessionStats } from '../contexts/SessionContext.js';
import { Table, type Column } from './Table.js';
const METRIC_COL_WIDTH = 28;
const MODEL_COL_WIDTH = 22;
interface StatRowProps {
title: string;
values: Array<string | React.ReactElement>;
isSubtle?: boolean;
interface StatRowData {
metric: string;
isSection?: boolean;
isSubtle?: boolean;
// Dynamic keys for model values
[key: string]: string | React.ReactNode | boolean | undefined;
}
const StatRow: React.FC<StatRowProps> = ({
title,
values,
isSubtle = false,
isSection = false,
}) => (
<Box>
<Box width={METRIC_COL_WIDTH}>
<Text
bold={isSection}
color={isSection ? theme.text.primary : theme.text.link}
>
{isSubtle ? `${title}` : title}
</Text>
</Box>
{values.map((value, index) => (
<Box width={MODEL_COL_WIDTH} key={index}>
<Text color={theme.text.primary}>{value}</Text>
</Box>
))}
</Box>
);
export const ModelStatsDisplay: React.FC = () => {
const { stats } = useSessionStats();
const { models } = stats.metrics;
@@ -73,10 +48,6 @@ export const ModelStatsDisplay: React.FC = () => {
const modelNames = activeModels.map(([name]) => name);
const getModelValues = (
getter: (metrics: ModelMetrics) => string | React.ReactElement,
) => activeModels.map(([, metrics]) => getter(metrics));
const hasThoughts = activeModels.some(
([, metrics]) => metrics.tokens.thoughts > 0,
);
@@ -85,6 +56,152 @@ export const ModelStatsDisplay: React.FC = () => {
([, metrics]) => metrics.tokens.cached > 0,
);
// Helper to create a row with values for each model
const createRow = (
metric: string,
getValue: (
metrics: (typeof activeModels)[0][1],
) => 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;
};
const rows: StatRowData[] = [
// API Section
{ metric: 'API', isSection: true },
createRow('Requests', (m) => m.api.totalRequests.toLocaleString()),
createRow('Errors', (m) => {
const errorRate = calculateErrorRate(m);
return (
<Text
color={
m.api.totalErrors > 0 ? theme.status.error : theme.text.primary
}
>
{m.api.totalErrors.toLocaleString()} ({errorRate.toFixed(1)}%)
</Text>
);
}),
createRow('Avg Latency', (m) => formatDuration(calculateAverageLatency(m))),
// Spacer
{ metric: '' },
// Tokens Section
{ metric: 'Tokens', isSection: true },
createRow('Total', (m) => (
<Text color={theme.text.secondary}>
{m.tokens.total.toLocaleString()}
</Text>
)),
createRow(
'Input',
(m) => (
<Text color={theme.text.primary}>
{m.tokens.input.toLocaleString()}
</Text>
),
{ isSubtle: true },
),
];
if (hasCached) {
rows.push(
createRow(
'Cache Reads',
(m) => {
const cacheHitRate = calculateCacheHitRate(m);
return (
<Text color={theme.text.secondary}>
{m.tokens.cached.toLocaleString()} ({cacheHitRate.toFixed(1)}%)
</Text>
);
},
{ isSubtle: true },
),
);
}
if (hasThoughts) {
rows.push(
createRow(
'Thoughts',
(m) => (
<Text color={theme.text.primary}>
{m.tokens.thoughts.toLocaleString()}
</Text>
),
{ isSubtle: true },
),
);
}
if (hasTool) {
rows.push(
createRow(
'Tool',
(m) => (
<Text color={theme.text.primary}>
{m.tokens.tool.toLocaleString()}
</Text>
),
{ isSubtle: true },
),
);
}
rows.push(
createRow(
'Output',
(m) => (
<Text color={theme.text.primary}>
{m.tokens.candidates.toLocaleString()}
</Text>
),
{ isSubtle: true },
),
);
const columns: Array<Column<StatRowData>> = [
{
key: 'metric',
header: 'Metric',
width: 28,
renderCell: (row) => (
<Text
bold={row.isSection}
color={row.isSection ? theme.text.primary : theme.text.link}
>
{row.isSubtle ? `${row.metric}` : row.metric}
</Text>
),
},
...modelNames.map((name) => ({
key: name,
header: name,
flexGrow: 1,
renderCell: (row: StatRowData) => {
// Don't render anything for section headers in model columns
if (row.isSection) return null;
const val = row[name];
if (val === undefined || val === null) return null;
if (typeof val === 'string' || typeof val === 'number') {
return <Text color={theme.text.primary}>{val}</Text>;
}
return val as React.ReactNode;
},
})),
];
return (
<Box
borderStyle="round"
@@ -97,128 +214,7 @@ export const ModelStatsDisplay: React.FC = () => {
Model Stats For Nerds
</Text>
<Box height={1} />
{/* Header */}
<Box>
<Box width={METRIC_COL_WIDTH}>
<Text bold color={theme.text.primary}>
Metric
</Text>
</Box>
{modelNames.map((name) => (
<Box width={MODEL_COL_WIDTH} key={name}>
<Text bold color={theme.text.primary}>
{name}
</Text>
</Box>
))}
</Box>
{/* Divider */}
<Box
borderStyle="single"
borderBottom={true}
borderTop={false}
borderLeft={false}
borderRight={false}
borderColor={theme.border.default}
/>
{/* API Section */}
<StatRow title="API" values={[]} isSection />
<StatRow
title="Requests"
values={getModelValues((m) => m.api.totalRequests.toLocaleString())}
/>
<StatRow
title="Errors"
values={getModelValues((m) => {
const errorRate = calculateErrorRate(m);
return (
<Text
color={
m.api.totalErrors > 0 ? theme.status.error : theme.text.primary
}
>
{m.api.totalErrors.toLocaleString()} ({errorRate.toFixed(1)}%)
</Text>
);
})}
/>
<StatRow
title="Avg Latency"
values={getModelValues((m) => {
const avgLatency = calculateAverageLatency(m);
return formatDuration(avgLatency);
})}
/>
<Box height={1} />
{/* Tokens Section */}
<StatRow title="Tokens" values={[]} isSection />
<StatRow
title="Total"
values={getModelValues((m) => (
<Text color={theme.text.secondary}>
{m.tokens.total.toLocaleString()}
</Text>
))}
/>
<StatRow
title="Input"
isSubtle
values={getModelValues((m) => (
<Text color={theme.text.primary}>
{m.tokens.input.toLocaleString()}
</Text>
))}
/>
{hasCached && (
<StatRow
title="Cache Reads"
isSubtle
values={getModelValues((m) => {
const cacheHitRate = calculateCacheHitRate(m);
return (
<Text color={theme.text.secondary}>
{m.tokens.cached.toLocaleString()} ({cacheHitRate.toFixed(1)}%)
</Text>
);
})}
/>
)}
{hasThoughts && (
<StatRow
title="Thoughts"
isSubtle
values={getModelValues((m) => (
<Text color={theme.text.primary}>
{m.tokens.thoughts.toLocaleString()}
</Text>
))}
/>
)}
{hasTool && (
<StatRow
title="Tool"
isSubtle
values={getModelValues((m) => (
<Text color={theme.text.primary}>
{m.tokens.tool.toLocaleString()}
</Text>
))}
/>
)}
<StatRow
title="Output"
isSubtle
values={getModelValues((m) => (
<Text color={theme.text.primary}>
{m.tokens.candidates.toLocaleString()}
</Text>
))}
/>
<Table data={rows} columns={columns} />
</Box>
);
};