mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
feat: display quota stats for unused models in /stats (#14764)
This commit is contained in:
@@ -77,7 +77,6 @@ describe('<StatsDisplay />', () => {
|
||||
expect(output).toContain('Performance');
|
||||
expect(output).toContain('Interaction Summary');
|
||||
expect(output).not.toContain('Efficiency & Optimizations');
|
||||
expect(output).not.toContain('Model'); // The table header
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -450,5 +449,51 @@ describe('<StatsDisplay />', () => {
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('renders quota information for unused models', () => {
|
||||
const now = new Date('2025-01-01T12:00:00Z');
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(now);
|
||||
|
||||
// No models in metrics, but a quota for gemini-2.5-flash
|
||||
const metrics = createTestMetrics();
|
||||
|
||||
const resetTime = new Date(now.getTime() + 1000 * 60 * 120).toISOString(); // 2 hours from now
|
||||
|
||||
const quotas: RetrieveUserQuotaResponse = {
|
||||
buckets: [
|
||||
{
|
||||
modelId: 'gemini-2.5-flash',
|
||||
remainingFraction: 0.5,
|
||||
resetTime,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
useSessionStatsMock.mockReturnValue({
|
||||
stats: {
|
||||
sessionId: 'test-session-id',
|
||||
sessionStartTime: new Date(),
|
||||
metrics,
|
||||
lastPromptTokenCount: 0,
|
||||
promptCount: 5,
|
||||
},
|
||||
getPromptCount: () => 5,
|
||||
startNewPrompt: vi.fn(),
|
||||
});
|
||||
|
||||
const { lastFrame } = render(
|
||||
<StatsDisplay duration="1s" quotas={quotas} />,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('gemini-2.5-flash');
|
||||
expect(output).toContain('-'); // for requests
|
||||
expect(output).toContain('50.0%');
|
||||
expect(output).toContain('(Resets in 2h)');
|
||||
expect(output).toMatchSnapshot();
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,10 @@ import {
|
||||
USER_AGREEMENT_RATE_MEDIUM,
|
||||
} from '../utils/displayUtils.js';
|
||||
import { computeSessionStats } from '../utils/computeStats.js';
|
||||
import type { RetrieveUserQuotaResponse } from '@google/gemini-cli-core';
|
||||
import {
|
||||
type RetrieveUserQuotaResponse,
|
||||
VALID_GEMINI_MODELS,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
// A more flexible and powerful StatRow component
|
||||
interface StatRowProps {
|
||||
@@ -70,6 +73,50 @@ const Section: React.FC<SectionProps> = ({ title, children }) => (
|
||||
</Box>
|
||||
);
|
||||
|
||||
// Logic for building the unified list of table rows
|
||||
const buildModelRows = (
|
||||
models: Record<string, ModelMetrics>,
|
||||
quotas?: RetrieveUserQuotaResponse,
|
||||
) => {
|
||||
const getBaseModelName = (name: string) => name.replace('-001', '');
|
||||
const usedModelNames = new Set(Object.keys(models).map(getBaseModelName));
|
||||
|
||||
// 1. Models with active usage
|
||||
const activeRows = Object.entries(models).map(([name, metrics]) => {
|
||||
const modelName = getBaseModelName(name);
|
||||
return {
|
||||
key: name,
|
||||
modelName,
|
||||
requests: metrics.api.totalRequests,
|
||||
inputTokens: metrics.tokens.prompt.toLocaleString(),
|
||||
outputTokens: metrics.tokens.candidates.toLocaleString(),
|
||||
bucket: quotas?.buckets?.find((b) => b.modelId === modelName),
|
||||
isActive: true,
|
||||
};
|
||||
});
|
||||
|
||||
// 2. Models with quota only
|
||||
const quotaRows =
|
||||
quotas?.buckets
|
||||
?.filter(
|
||||
(b) =>
|
||||
b.modelId &&
|
||||
VALID_GEMINI_MODELS.has(b.modelId) &&
|
||||
!usedModelNames.has(b.modelId),
|
||||
)
|
||||
.map((bucket) => ({
|
||||
key: bucket.modelId!,
|
||||
modelName: bucket.modelId!,
|
||||
requests: '-',
|
||||
inputTokens: '-',
|
||||
outputTokens: '-',
|
||||
bucket,
|
||||
isActive: false,
|
||||
})) || [];
|
||||
|
||||
return [...activeRows, ...quotaRows];
|
||||
};
|
||||
|
||||
const formatResetTime = (resetTime: string): string => {
|
||||
const diff = new Date(resetTime).getTime() - Date.now();
|
||||
if (diff <= 0) return '';
|
||||
@@ -100,6 +147,12 @@ const ModelUsageTable: React.FC<{
|
||||
cacheEfficiency: number;
|
||||
quotas?: RetrieveUserQuotaResponse;
|
||||
}> = ({ models, totalCachedTokens, cacheEfficiency, quotas }) => {
|
||||
const rows = buildModelRows(models, quotas);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nameWidth = 25;
|
||||
const requestsWidth = 8;
|
||||
const inputTokensWidth = 15;
|
||||
@@ -138,6 +191,7 @@ const ModelUsageTable: React.FC<{
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Divider */}
|
||||
<Box
|
||||
borderStyle="round"
|
||||
@@ -155,44 +209,45 @@ const ModelUsageTable: React.FC<{
|
||||
}
|
||||
></Box>
|
||||
|
||||
{/* Rows */}
|
||||
{Object.entries(models).map(([name, modelMetrics]) => {
|
||||
const modelName = name.replace('-001', '');
|
||||
const bucket = quotas?.buckets?.find((b) => b.modelId === modelName);
|
||||
|
||||
return (
|
||||
<Box key={name}>
|
||||
<Box width={nameWidth}>
|
||||
<Text color={theme.text.primary}>{modelName}</Text>
|
||||
</Box>
|
||||
<Box width={requestsWidth} justifyContent="flex-end">
|
||||
<Text color={theme.text.primary}>
|
||||
{modelMetrics.api.totalRequests}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={inputTokensWidth} justifyContent="flex-end">
|
||||
<Text color={theme.status.warning}>
|
||||
{modelMetrics.tokens.prompt.toLocaleString()}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={outputTokensWidth} justifyContent="flex-end">
|
||||
<Text color={theme.status.warning}>
|
||||
{modelMetrics.tokens.candidates.toLocaleString()}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={usageLimitWidth} justifyContent="flex-end">
|
||||
{bucket &&
|
||||
bucket.remainingFraction != null &&
|
||||
bucket.resetTime && (
|
||||
<Text color={theme.text.secondary}>
|
||||
{(bucket.remainingFraction * 100).toFixed(1)}%{' '}
|
||||
{formatResetTime(bucket.resetTime)}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
{rows.map((row) => (
|
||||
<Box key={row.key}>
|
||||
<Box width={nameWidth}>
|
||||
<Text color={theme.text.primary}>{row.modelName}</Text>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
<Box width={requestsWidth} justifyContent="flex-end">
|
||||
<Text
|
||||
color={row.isActive ? theme.text.primary : theme.text.secondary}
|
||||
>
|
||||
{row.requests}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={inputTokensWidth} justifyContent="flex-end">
|
||||
<Text
|
||||
color={row.isActive ? theme.status.warning : theme.text.secondary}
|
||||
>
|
||||
{row.inputTokens}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={outputTokensWidth} justifyContent="flex-end">
|
||||
<Text
|
||||
color={row.isActive ? theme.status.warning : theme.text.secondary}
|
||||
>
|
||||
{row.outputTokens}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={usageLimitWidth} justifyContent="flex-end">
|
||||
{row.bucket &&
|
||||
row.bucket.remainingFraction != null &&
|
||||
row.bucket.resetTime && (
|
||||
<Text color={theme.text.secondary}>
|
||||
{(row.bucket.remainingFraction * 100).toFixed(1)}%{' '}
|
||||
{formatResetTime(row.bucket.resetTime)}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
{cacheEfficiency > 0 && (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text color={theme.text.primary}>
|
||||
@@ -202,6 +257,7 @@ const ModelUsageTable: React.FC<{
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{models && (
|
||||
<>
|
||||
<Box marginTop={1} marginBottom={2}>
|
||||
@@ -335,15 +391,12 @@ export const StatsDisplay: React.FC<StatsDisplayProps> = ({
|
||||
</Text>
|
||||
</SubStatRow>
|
||||
</Section>
|
||||
|
||||
{Object.keys(models).length > 0 && (
|
||||
<ModelUsageTable
|
||||
models={models}
|
||||
totalCachedTokens={computed.totalCachedTokens}
|
||||
cacheEfficiency={computed.cacheEfficiency}
|
||||
quotas={quotas}
|
||||
/>
|
||||
)}
|
||||
<ModelUsageTable
|
||||
models={models}
|
||||
totalCachedTokens={computed.totalCachedTokens}
|
||||
cacheEfficiency={computed.cacheEfficiency}
|
||||
quotas={quotas}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -151,6 +151,36 @@ exports[`<StatsDisplay /> > Conditional Rendering Tests > hides User Agreement w
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<StatsDisplay /> > Quota Display > renders quota information for unused 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: 0s │
|
||||
│ » API Time: 0s (0.0%) │
|
||||
│ » Tool Time: 0s (0.0%) │
|
||||
│ │
|
||||
│ │
|
||||
│ Model Usage Reqs Input Tokens Output Tokens Usage limit remaining │
|
||||
│ ───────────────────────────────────────────────────────────────────────────────────────────── │
|
||||
│ gemini-2.5-flash - - - 50.0% (Resets in 2h) │
|
||||
│ │
|
||||
│ Usage limits span all sessions and reset daily. │
|
||||
│ /auth to upgrade or switch to API key. │
|
||||
│ │
|
||||
│ │
|
||||
│ » Tip: For a full token breakdown, run \`/stats model\`. │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<StatsDisplay /> > Quota Display > renders quota information when quotas are provided 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
|
||||
@@ -9,6 +9,13 @@ export const DEFAULT_GEMINI_MODEL = 'gemini-2.5-pro';
|
||||
export const DEFAULT_GEMINI_FLASH_MODEL = 'gemini-2.5-flash';
|
||||
export const DEFAULT_GEMINI_FLASH_LITE_MODEL = 'gemini-2.5-flash-lite';
|
||||
|
||||
export const VALID_GEMINI_MODELS = new Set([
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||
]);
|
||||
|
||||
export const DEFAULT_GEMINI_MODEL_AUTO = 'auto';
|
||||
|
||||
// Model aliases for user convenience.
|
||||
|
||||
Reference in New Issue
Block a user