diff --git a/packages/cli/src/ui/components/StatsDisplay.test.tsx b/packages/cli/src/ui/components/StatsDisplay.test.tsx
index de6a33caf9..4e79bd4fc8 100644
--- a/packages/cli/src/ui/components/StatsDisplay.test.tsx
+++ b/packages/cli/src/ui/components/StatsDisplay.test.tsx
@@ -77,7 +77,6 @@ describe('', () => {
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('', () => {
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(
+ ,
+ );
+ 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();
+ });
});
});
diff --git a/packages/cli/src/ui/components/StatsDisplay.tsx b/packages/cli/src/ui/components/StatsDisplay.tsx
index 7290640ba6..5aaaeaef2c 100644
--- a/packages/cli/src/ui/components/StatsDisplay.tsx
+++ b/packages/cli/src/ui/components/StatsDisplay.tsx
@@ -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 = ({ title, children }) => (
);
+// Logic for building the unified list of table rows
+const buildModelRows = (
+ models: Record,
+ 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<{
)}
+
{/* Divider */}
- {/* Rows */}
- {Object.entries(models).map(([name, modelMetrics]) => {
- const modelName = name.replace('-001', '');
- const bucket = quotas?.buckets?.find((b) => b.modelId === modelName);
-
- return (
-
-
- {modelName}
-
-
-
- {modelMetrics.api.totalRequests}
-
-
-
-
- {modelMetrics.tokens.prompt.toLocaleString()}
-
-
-
-
- {modelMetrics.tokens.candidates.toLocaleString()}
-
-
-
- {bucket &&
- bucket.remainingFraction != null &&
- bucket.resetTime && (
-
- {(bucket.remainingFraction * 100).toFixed(1)}%{' '}
- {formatResetTime(bucket.resetTime)}
-
- )}
-
+ {rows.map((row) => (
+
+
+ {row.modelName}
- );
- })}
+
+
+ {row.requests}
+
+
+
+
+ {row.inputTokens}
+
+
+
+
+ {row.outputTokens}
+
+
+
+ {row.bucket &&
+ row.bucket.remainingFraction != null &&
+ row.bucket.resetTime && (
+
+ {(row.bucket.remainingFraction * 100).toFixed(1)}%{' '}
+ {formatResetTime(row.bucket.resetTime)}
+
+ )}
+
+
+ ))}
+
{cacheEfficiency > 0 && (
@@ -202,6 +257,7 @@ const ModelUsageTable: React.FC<{
)}
+
{models && (
<>
@@ -335,15 +391,12 @@ export const StatsDisplay: React.FC = ({
-
- {Object.keys(models).length > 0 && (
-
- )}
+
);
};
diff --git a/packages/cli/src/ui/components/__snapshots__/StatsDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/StatsDisplay.test.tsx.snap
index ded84b667f..1e0b3597d3 100644
--- a/packages/cli/src/ui/components/__snapshots__/StatsDisplay.test.tsx.snap
+++ b/packages/cli/src/ui/components/__snapshots__/StatsDisplay.test.tsx.snap
@@ -151,6 +151,36 @@ exports[` > Conditional Rendering Tests > hides User Agreement w
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
+exports[` > 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[` > Quota Display > renders quota information when quotas are provided 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │
diff --git a/packages/core/src/config/models.ts b/packages/core/src/config/models.ts
index f3afad61bc..215019ef03 100644
--- a/packages/core/src/config/models.ts
+++ b/packages/core/src/config/models.ts
@@ -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.