feat(cli): add role specific metrics to /stats (#24659)

This commit is contained in:
cynthialong0-0
2026-04-06 09:20:48 -07:00
committed by GitHub
parent 15298b28c2
commit c96cb09e09
4 changed files with 167 additions and 14 deletions

View File

@@ -9,7 +9,7 @@ import { describe, it, expect, vi } from 'vitest';
import { StatsDisplay } from './StatsDisplay.js';
import * as SessionContext from '../contexts/SessionContext.js';
import { type SessionMetrics } from '../contexts/SessionContext.js';
import { ToolCallDecision } from '@google/gemini-cli-core';
import { ToolCallDecision, LlmRole } from '@google/gemini-cli-core';
// Mock the context to provide controlled data for testing
vi.mock('../contexts/SessionContext.js', async (importOriginal) => {
@@ -131,6 +131,66 @@ describe('<StatsDisplay />', () => {
expect(output).toMatchSnapshot();
});
it('renders role breakdown correctly under models', async () => {
const metrics = createTestMetrics({
models: {
'gemini-2.5-flash': {
api: { totalRequests: 10, totalErrors: 0, totalLatencyMs: 10000 },
tokens: {
input: 1000,
prompt: 1200,
candidates: 2000,
total: 3200,
cached: 200,
thoughts: 0,
tool: 0,
},
roles: {
[LlmRole.MAIN]: {
totalRequests: 7,
totalErrors: 0,
totalLatencyMs: 7000,
tokens: {
input: 800,
prompt: 900,
candidates: 1500,
total: 2400,
cached: 100,
thoughts: 0,
tool: 0,
},
},
[LlmRole.UTILITY_TOOL]: {
totalRequests: 3,
totalErrors: 0,
totalLatencyMs: 3000,
tokens: {
input: 200,
prompt: 300,
candidates: 500,
total: 800,
cached: 100,
thoughts: 0,
tool: 0,
},
},
},
},
},
});
const { lastFrame } = await renderWithMockedStats(metrics);
const output = lastFrame();
expect(output).toContain('gemini-2.5-flash');
expect(output).toContain('10'); // Total requests
expect(output).toContain('↳ main');
expect(output).toContain('7'); // main requests
expect(output).toContain('↳ utility_tool');
expect(output).toContain('3'); // tool requests
expect(output).toMatchSnapshot();
});
it('renders all sections when all data is present', async () => {
const metrics = createTestMetrics({
models: {

View File

@@ -12,6 +12,7 @@ import { formatDuration } from '../utils/formatters.js';
import {
useSessionStats,
type ModelMetrics,
type RoleMetrics,
} from '../contexts/SessionContext.js';
import {
getStatusColor,
@@ -23,6 +24,7 @@ import {
import { computeSessionStats } from '../utils/computeStats.js';
import { useSettings } from '../contexts/SettingsContext.js';
import type { QuotaStats } from '../types.js';
import { LlmRole } from '@google/gemini-cli-core';
// A more flexible and powerful StatRow component
interface StatRowProps {
@@ -77,6 +79,16 @@ interface ModelUsageTableProps {
models: Record<string, ModelMetrics>;
}
interface ModelRow {
name: string;
displayName: string;
requests: number | string;
cachedTokens: string;
inputTokens: string;
outputTokens: string;
isSubRow: boolean;
}
const ModelUsageTable: React.FC<ModelUsageTableProps> = ({ models }) => {
const nameWidth = 28;
const requestsWidth = 8;
@@ -84,6 +96,46 @@ const ModelUsageTable: React.FC<ModelUsageTableProps> = ({ models }) => {
const cacheReadsWidth = 14;
const outputTokensWidth = 14;
const rows: ModelRow[] = [];
Object.entries(models).forEach(([name, metrics]) => {
rows.push({
name,
displayName: name,
requests: metrics.api.totalRequests,
cachedTokens: metrics.tokens.cached.toLocaleString(),
inputTokens: metrics.tokens.prompt.toLocaleString(),
outputTokens: metrics.tokens.candidates.toLocaleString(),
isSubRow: false,
});
if (metrics.roles) {
const roleEntries = Object.entries(metrics.roles).filter(
(entry): entry is [string, RoleMetrics] =>
entry[1] !== undefined && entry[1].totalRequests > 0,
);
roleEntries.sort(([a], [b]) => {
if (a === b) return 0;
if (a === LlmRole.MAIN) return -1;
if (b === LlmRole.MAIN) return 1;
return a.localeCompare(b);
});
roleEntries.forEach(([role, roleMetrics]) => {
rows.push({
name: `${name}-${role}`,
displayName: `${role}`,
requests: roleMetrics.totalRequests,
cachedTokens: roleMetrics.tokens.cached.toLocaleString(),
inputTokens: roleMetrics.tokens.prompt.toLocaleString(),
outputTokens: roleMetrics.tokens.candidates.toLocaleString(),
isSubRow: true,
});
});
}
});
return (
<Box flexDirection="column" marginTop={1}>
<Text bold color={theme.text.primary}>
@@ -131,31 +183,42 @@ const ModelUsageTable: React.FC<ModelUsageTableProps> = ({ models }) => {
</Box>
{/* Rows */}
{Object.entries(models).map(([name, modelMetrics]) => (
<Box key={name}>
{rows.map((row) => (
<Box key={row.name}>
<Box width={nameWidth}>
<Text color={theme.text.primary} wrap="truncate-end">
{name}
<Text
color={row.isSubRow ? theme.text.secondary : theme.text.primary}
wrap="truncate-end"
>
{row.displayName}
</Text>
</Box>
<Box width={requestsWidth} justifyContent="flex-end">
<Text color={theme.text.primary}>
{modelMetrics.api.totalRequests}
<Text
color={row.isSubRow ? theme.text.secondary : theme.text.primary}
>
{row.requests}
</Text>
</Box>
<Box width={inputTokensWidth} justifyContent="flex-end">
<Text color={theme.text.primary}>
{modelMetrics.tokens.prompt.toLocaleString()}
<Text
color={row.isSubRow ? theme.text.secondary : theme.text.primary}
>
{row.inputTokens}
</Text>
</Box>
<Box width={cacheReadsWidth} justifyContent="flex-end">
<Text color={theme.text.primary}>
{modelMetrics.tokens.cached.toLocaleString()}
<Text
color={row.isSubRow ? theme.text.secondary : theme.text.primary}
>
{row.cachedTokens}
</Text>
</Box>
<Box width={outputTokensWidth} justifyContent="flex-end">
<Text color={theme.text.primary}>
{modelMetrics.tokens.candidates.toLocaleString()}
<Text
color={row.isSubRow ? theme.text.secondary : theme.text.primary}
>
{row.outputTokens}
</Text>
</Box>
</Box>

View File

@@ -263,3 +263,32 @@ exports[`<StatsDisplay /> > renders only the Performance section in its zero sta
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
exports[`<StatsDisplay /> > renders role breakdown correctly under models 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │
│ Session Stats │
│ │
│ Interaction Summary │
│ Session ID: test-session-id │
│ Tool Calls: 0 ( ✓ 0 x 0 ) │
│ Success Rate: 0.0% │
│ │
│ Performance │
│ Wall Time: 1s │
│ Agent Active: 10.0s │
│ » API Time: 10.0s (100.0%) │
│ » Tool Time: 0s (0.0%) │
│ │
│ │
│ Model Usage │
│ Use /model to view model quota information │
│ │
│ Model Reqs Input Tokens Cache Reads Output Tokens │
│ ────────────────────────────────────────────────────────────────────────────────────────────── │
│ gemini-2.5-flash 10 1,200 200 2,000 │
│ ↳ main 7 900 100 1,500 │
│ ↳ utility_tool 3 300 100 500 │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;

View File

@@ -17,6 +17,7 @@ import {
import type {
SessionMetrics,
ModelMetrics,
RoleMetrics,
ToolCallStats,
} from '@google/gemini-cli-core';
import { uiTelemetryService, sessionId } from '@google/gemini-cli-core';
@@ -139,7 +140,7 @@ function areMetricsEqual(a: SessionMetrics, b: SessionMetrics): boolean {
return true;
}
export type { SessionMetrics, ModelMetrics };
export type { SessionMetrics, ModelMetrics, RoleMetrics };
export interface SessionStatsState {
sessionId: string;