summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/components/ModelStatsDisplay.tsx
diff options
context:
space:
mode:
authorAbhi <[email protected]>2025-06-29 20:44:33 -0400
committerGitHub <[email protected]>2025-06-30 00:44:33 +0000
commit770f862832dfef477705bee69bd2a84397d105a8 (patch)
tree8cb647cf789f05458ff491b461aa531a6932ad3d /packages/cli/src/ui/components/ModelStatsDisplay.tsx
parent0fd602eb43eea7abca980dc2ae3fd7bf2ba76a2a (diff)
feat: Change /stats to include more detailed breakdowns (#2615)
Diffstat (limited to 'packages/cli/src/ui/components/ModelStatsDisplay.tsx')
-rw-r--r--packages/cli/src/ui/components/ModelStatsDisplay.tsx197
1 files changed, 197 insertions, 0 deletions
diff --git a/packages/cli/src/ui/components/ModelStatsDisplay.tsx b/packages/cli/src/ui/components/ModelStatsDisplay.tsx
new file mode 100644
index 00000000..1911e757
--- /dev/null
+++ b/packages/cli/src/ui/components/ModelStatsDisplay.tsx
@@ -0,0 +1,197 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Box, Text } from 'ink';
+import { Colors } from '../colors.js';
+import { formatDuration } from '../utils/formatters.js';
+import {
+ calculateAverageLatency,
+ calculateCacheHitRate,
+ calculateErrorRate,
+} from '../utils/computeStats.js';
+import { useSessionStats, ModelMetrics } from '../contexts/SessionContext.js';
+
+const METRIC_COL_WIDTH = 28;
+const MODEL_COL_WIDTH = 22;
+
+interface StatRowProps {
+ title: string;
+ values: Array<string | React.ReactElement>;
+ isSubtle?: boolean;
+ isSection?: boolean;
+}
+
+const StatRow: React.FC<StatRowProps> = ({
+ title,
+ values,
+ isSubtle = false,
+ isSection = false,
+}) => (
+ <Box>
+ <Box width={METRIC_COL_WIDTH}>
+ <Text bold={isSection} color={isSection ? undefined : Colors.LightBlue}>
+ {isSubtle ? ` ↳ ${title}` : title}
+ </Text>
+ </Box>
+ {values.map((value, index) => (
+ <Box width={MODEL_COL_WIDTH} key={index}>
+ <Text>{value}</Text>
+ </Box>
+ ))}
+ </Box>
+);
+
+export const ModelStatsDisplay: React.FC = () => {
+ const { stats } = useSessionStats();
+ const { models } = stats.metrics;
+ const activeModels = Object.entries(models).filter(
+ ([, metrics]) => metrics.api.totalRequests > 0,
+ );
+
+ if (activeModels.length === 0) {
+ return (
+ <Box
+ borderStyle="round"
+ borderColor={Colors.Gray}
+ paddingY={1}
+ paddingX={2}
+ >
+ <Text>No API calls have been made in this session.</Text>
+ </Box>
+ );
+ }
+
+ 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,
+ );
+ const hasTool = activeModels.some(([, metrics]) => metrics.tokens.tool > 0);
+ const hasCached = activeModels.some(
+ ([, metrics]) => metrics.tokens.cached > 0,
+ );
+
+ return (
+ <Box
+ borderStyle="round"
+ borderColor={Colors.Gray}
+ flexDirection="column"
+ paddingY={1}
+ paddingX={2}
+ >
+ <Text bold color={Colors.AccentPurple}>
+ Model Stats For Nerds
+ </Text>
+ <Box height={1} />
+
+ {/* Header */}
+ <Box>
+ <Box width={METRIC_COL_WIDTH}>
+ <Text bold>Metric</Text>
+ </Box>
+ {modelNames.map((name) => (
+ <Box width={MODEL_COL_WIDTH} key={name}>
+ <Text bold>{name}</Text>
+ </Box>
+ ))}
+ </Box>
+
+ {/* Divider */}
+ <Box
+ borderStyle="single"
+ borderBottom={true}
+ borderTop={false}
+ borderLeft={false}
+ borderRight={false}
+ />
+
+ {/* 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 ? Colors.AccentRed : Colors.Foreground
+ }
+ >
+ {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={Colors.AccentYellow}>
+ {m.tokens.total.toLocaleString()}
+ </Text>
+ ))}
+ />
+ <StatRow
+ title="Prompt"
+ isSubtle
+ values={getModelValues((m) => m.tokens.prompt.toLocaleString())}
+ />
+ {hasCached && (
+ <StatRow
+ title="Cached"
+ isSubtle
+ values={getModelValues((m) => {
+ const cacheHitRate = calculateCacheHitRate(m);
+ return (
+ <Text color={Colors.AccentGreen}>
+ {m.tokens.cached.toLocaleString()} ({cacheHitRate.toFixed(1)}%)
+ </Text>
+ );
+ })}
+ />
+ )}
+ {hasThoughts && (
+ <StatRow
+ title="Thoughts"
+ isSubtle
+ values={getModelValues((m) => m.tokens.thoughts.toLocaleString())}
+ />
+ )}
+ {hasTool && (
+ <StatRow
+ title="Tool"
+ isSubtle
+ values={getModelValues((m) => m.tokens.tool.toLocaleString())}
+ />
+ )}
+ <StatRow
+ title="Output"
+ isSubtle
+ values={getModelValues((m) => m.tokens.candidates.toLocaleString())}
+ />
+ </Box>
+ );
+};