feat: add role-specific statistics to telemetry and UI (cont. #15234) (#18824)

Co-authored-by: Yuna Seol <yunaseol@google.com>
This commit is contained in:
Yuna Seol
2026-02-17 12:32:30 -05:00
committed by GitHub
parent 14aabbbe8b
commit 8aca3068cf
51 changed files with 826 additions and 20 deletions

View File

@@ -13,10 +13,17 @@ import {
calculateCacheHitRate,
calculateErrorRate,
} from '../utils/computeStats.js';
import { useSessionStats } from '../contexts/SessionContext.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 } from '@google/gemini-cli-core';
import {
getDisplayString,
isAutoModel,
LlmRole,
} from '@google/gemini-cli-core';
import type { QuotaStats } from '../types.js';
import { QuotaStatsInfo } from './QuotaStatsInfo.js';
@@ -25,9 +32,11 @@ interface StatRowData {
isSection?: boolean;
isSubtle?: boolean;
// Dynamic keys for model values
[key: string]: string | React.ReactNode | boolean | undefined;
[key: string]: string | React.ReactNode | boolean | undefined | number;
}
type RoleMetrics = NonNullable<NonNullable<ModelMetrics['roles']>[LlmRole]>;
interface ModelStatsDisplayProps {
selectedAuthType?: string;
userEmail?: string;
@@ -81,6 +90,22 @@ export const ModelStatsDisplay: React.FC<ModelStatsDisplayProps> = ({
([, 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,
@@ -204,6 +229,60 @@ export const ModelStatsDisplay: React.FC<ModelStatsDisplayProps> = ({
),
);
// 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] = <Text color={theme.text.secondary}>-</Text>;
}
});
rows.push(row);
};
addRoleMetric('Requests', (r) => r.totalRequests.toLocaleString());
addRoleMetric('Input', (r) => (
<Text color={theme.text.primary}>
{r.tokens.input.toLocaleString()}
</Text>
));
addRoleMetric('Output', (r) => (
<Text color={theme.text.primary}>
{r.tokens.candidates.toLocaleString()}
</Text>
));
addRoleMetric('Cache Reads', (r) => (
<Text color={theme.text.secondary}>
{r.tokens.cached.toLocaleString()}
</Text>
));
});
}
const columns: Array<Column<StatRowData>> = [
{
key: 'metric',