/** * @license * Copyright 2026 Google 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, calculateCacheHitRate, calculateErrorRate, } from '../utils/computeStats.js'; import { useSessionStats, type ModelMetrics, } from '../contexts/SessionContext.js'; import { Table, type Column } from './Table.js'; import { useSettings } from '../contexts/SettingsContext.js'; import { getDisplayString, isAutoModel, LlmRole, } from '@google/gemini-cli-core'; import type { QuotaStats } from '../types.js'; import { QuotaStatsInfo } from './QuotaStatsInfo.js'; interface StatRowData { metric: string; isSection?: boolean; isSubtle?: boolean; // Dynamic keys for model values [key: string]: string | React.ReactNode | boolean | undefined | number; } type RoleMetrics = NonNullable[LlmRole]>; interface ModelStatsDisplayProps { selectedAuthType?: string; userEmail?: string; tier?: string; currentModel?: string; quotaStats?: QuotaStats; } export const ModelStatsDisplay: React.FC = ({ selectedAuthType, userEmail, tier, currentModel, quotaStats, }) => { const { stats } = useSessionStats(); const pooledRemaining = quotaStats?.remaining; const pooledLimit = quotaStats?.limit; const pooledResetTime = quotaStats?.resetTime; const { models } = stats.metrics; const settings = useSettings(); const showUserIdentity = settings.merged.ui.showUserIdentity; const activeModels = Object.entries(models).filter( ([, metrics]) => metrics.api.totalRequests > 0, ); 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 allRoles = [ ...new Set( activeModels.flatMap(([, metrics]) => Object.keys(metrics.roles ?? {})), ), ] .filter((role): role is LlmRole => { const validRoles: string[] = Object.values(LlmRole); return validRoles.includes(role); }) .sort((a, b) => { if (a === b) return 0; if (a === LlmRole.MAIN) return -1; if (b === LlmRole.MAIN) return 1; return a.localeCompare(b); }); // 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 rows.push({ metric: 'API', isSection: true }); rows.push(createRow('Requests', (m) => m.api.totalRequests.toLocaleString())); rows.push( createRow('Errors', (m) => { const errorRate = calculateErrorRate(m); return ( 0 ? theme.status.error : theme.text.primary } > {m.api.totalErrors.toLocaleString()} ({errorRate.toFixed(1)}%) ); }), ); rows.push( createRow('Avg Latency', (m) => formatDuration(calculateAverageLatency(m))), ); // Spacer rows.push({ metric: '' }); // Tokens Section rows.push({ metric: 'Tokens', isSection: true }); rows.push( createRow('Total', (m) => ( {m.tokens.total.toLocaleString()} )), ); rows.push( createRow( 'Input', (m) => ( {m.tokens.input.toLocaleString()} ), { isSubtle: true }, ), ); if (hasCached) { rows.push( createRow( 'Cache Reads', (m) => { const cacheHitRate = calculateCacheHitRate(m); return ( {m.tokens.cached.toLocaleString()} ({cacheHitRate.toFixed(1)}%) ); }, { isSubtle: true }, ), ); } if (hasThoughts) { rows.push( createRow( 'Thoughts', (m) => ( {m.tokens.thoughts.toLocaleString()} ), { isSubtle: true }, ), ); } if (hasTool) { rows.push( createRow( 'Tool', (m) => ( {m.tokens.tool.toLocaleString()} ), { isSubtle: true }, ), ); } rows.push( createRow( 'Output', (m) => ( {m.tokens.candidates.toLocaleString()} ), { isSubtle: true }, ), ); // Roles Section if (allRoles.length > 0) { // Spacer rows.push({ metric: '' }); rows.push({ metric: 'Roles', isSection: true }); allRoles.forEach((role) => { // Role Header Row const roleHeaderRow: StatRowData = { metric: role, isSection: true, color: theme.text.primary, }; // We don't populate model values for the role header row rows.push(roleHeaderRow); const addRoleMetric = ( metric: string, getValue: (r: RoleMetrics) => string | React.ReactNode, ) => { const row: StatRowData = { metric, isSubtle: true, }; activeModels.forEach(([name, metrics]) => { const roleMetrics = metrics.roles?.[role]; if (roleMetrics) { row[name] = getValue(roleMetrics); } else { row[name] = -; } }); rows.push(row); }; addRoleMetric('Requests', (r) => r.totalRequests.toLocaleString()); addRoleMetric('Input', (r) => ( {r.tokens.input.toLocaleString()} )); addRoleMetric('Output', (r) => ( {r.tokens.candidates.toLocaleString()} )); addRoleMetric('Cache Reads', (r) => ( {r.tokens.cached.toLocaleString()} )); }); } const columns: Array> = [ { key: 'metric', header: 'Metric', width: 28, renderCell: (row) => ( {row.isSubtle ? ` ↳ ${row.metric}` : row.metric} ), }, ...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 {val}; } return val as React.ReactNode; }, })), ]; const isAuto = currentModel && isAutoModel(currentModel); const statsTitle = isAuto ? `${getDisplayString(currentModel)} Stats For Nerds` : 'Model Stats For Nerds'; return ( {statsTitle} {showUserIdentity && selectedAuthType && ( Auth Method: {selectedAuthType.startsWith('oauth') ? userEmail ? `Logged in with Google (${userEmail})` : 'Logged in with Google' : selectedAuthType} )} {showUserIdentity && tier && ( Tier: {tier} )} {isAuto && pooledRemaining !== undefined && pooledLimit !== undefined && pooledLimit > 0 && ( )} {(showUserIdentity || isAuto) && } ); };