mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-08 04:10:35 -07:00
feat(cli): add role specific metrics to /stats (#24659)
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user