feat(cliproxy): add entitlement evidence for gemini and agy

This commit is contained in:
Tam Nhu Tran
2026-04-10 13:17:50 -04:00
parent 70051c2acb
commit bb331ff5d8
14 changed files with 371 additions and 34 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ const GEMINI_CLI_QUOTA_GROUPS: GeminiCliQuotaGroupDefinition[] = [
id: 'gemini-flash-lite-series',
label: 'Gemini Flash Lite Series',
preferredModelId: 'gemini-2.5-flash-lite',
modelIds: ['gemini-2.5-flash-lite'],
modelIds: ['gemini-2.5-flash-lite', 'gemini-3.1-flash-lite-preview'],
},
{
id: 'gemini-flash-series',
@@ -0,0 +1,69 @@
import type { AccountTier } from './accounts/types';
import type {
ProviderAccessState,
ProviderCapacityState,
ProviderEntitlementEvidence,
ProviderEntitlementSource,
} from './provider-entitlement-types';
const RAW_TIER_LABELS: Record<string, string> = {
'free-tier': 'Free',
'legacy-tier': 'Legacy',
'standard-tier': 'Standard',
'g1-pro-tier': 'Pro',
'g1-ultra-tier': 'Ultra',
};
export function normalizeProviderTierId(rawTierId: string | null | undefined): AccountTier {
if (!rawTierId) return 'unknown';
const normalized = rawTierId.trim().toLowerCase();
if (!normalized) return 'unknown';
if (normalized.includes('ultra')) return 'ultra';
if (normalized.includes('pro')) return 'pro';
if (normalized.includes('free') || normalized.includes('legacy')) return 'free';
return 'unknown';
}
export function getProviderTierLabel(rawTierId: string | null | undefined): string | null {
if (!rawTierId) return null;
const normalized = rawTierId.trim().toLowerCase();
return normalized ? (RAW_TIER_LABELS[normalized] ?? rawTierId.trim()) : null;
}
export function buildProviderEntitlementEvidence(input: {
normalizedTier: AccountTier;
rawTierId?: string | null;
rawTierLabel?: string | null;
source: ProviderEntitlementSource;
confidence: 'high' | 'medium' | 'low';
accessState: ProviderAccessState;
capacityState: ProviderCapacityState;
notes?: string | null;
lastVerifiedAt?: number;
}): ProviderEntitlementEvidence {
const rawTierId = input.rawTierId?.trim() || null;
return {
normalizedTier: input.normalizedTier,
rawTierId,
rawTierLabel: input.rawTierLabel ?? getProviderTierLabel(rawTierId),
source: input.source,
confidence: input.confidence,
accessState: input.accessState,
capacityState: input.capacityState,
notes: input.notes ?? null,
lastVerifiedAt: input.lastVerifiedAt ?? Date.now(),
};
}
export function isModelCapacityExhausted(
message: string | null | undefined,
detail: string | null | undefined,
errorCode: string | null | undefined
): boolean {
const haystack = `${message || ''} ${detail || ''} ${errorCode || ''}`.toLowerCase();
return (
haystack.includes('model_capacity_exhausted') ||
haystack.includes('no capacity available') ||
haystack.includes('capacity exhausted')
);
}
@@ -0,0 +1,33 @@
import type { AccountTier } from './accounts/types';
export type ProviderEntitlementSource =
| 'runtime_api'
| 'runtime_inference'
| 'registry_cache'
| 'official_docs';
export type ProviderAccessState =
| 'entitled'
| 'not_entitled'
| 'capacity_exhausted'
| 'temporarily_unavailable'
| 'unknown';
export type ProviderCapacityState =
| 'available'
| 'capacity_exhausted'
| 'rate_limited'
| 'temporarily_unavailable'
| 'unknown';
export interface ProviderEntitlementEvidence {
normalizedTier: AccountTier;
rawTierId: string | null;
rawTierLabel: string | null;
source: ProviderEntitlementSource;
confidence: 'high' | 'medium' | 'low';
accessState: ProviderAccessState;
capacityState: ProviderCapacityState;
lastVerifiedAt: number;
notes?: string | null;
}
+62 -12
View File
@@ -8,7 +8,7 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { getAuthDir } from './config-generator';
import { getProviderAccounts, getPausedDir } from './account-manager';
import { getProviderAccounts, getPausedDir, setAccountTier } from './account-manager';
import { getTokenExpiryTimestamp, sanitizeEmail, isTokenExpired } from './auth-utils';
import { refreshGeminiToken } from './auth/gemini-token-refresh';
import {
@@ -16,6 +16,13 @@ import {
type GeminiCliParsedBucket,
} from './gemini-cli-quota-normalizer';
import type { GeminiCliQuotaResult, GeminiCliBucket } from './quota-types';
import {
buildProviderEntitlementEvidence,
getProviderTierLabel,
isModelCapacityExhausted,
normalizeProviderTierId,
} from './provider-entitlement-evidence';
import type { ProviderEntitlementEvidence } from './provider-entitlement-types';
/** Google Cloud Code API endpoints */
const GEMINI_CLI_API_BASE = 'https://cloudcode-pa.googleapis.com';
@@ -25,13 +32,6 @@ const GEMINI_CLI_CODE_ASSIST_URL = `${GEMINI_CLI_API_BASE}/${GEMINI_CLI_API_VERS
const GEMINI_CLI_ERROR_DETAIL_MAX_LENGTH = 320;
const GEMINI_CLI_ERROR_DETAIL_TRUNCATION_SUFFIX = '...[truncated]';
const GEMINI_CLI_G1_CREDIT_TYPE = 'GOOGLE_ONE_AI';
const GEMINI_CLI_TIER_LABELS: Record<string, string> = {
'free-tier': 'Free',
'legacy-tier': 'Legacy',
'standard-tier': 'Standard',
'g1-pro-tier': 'Pro',
'g1-ultra-tier': 'Ultra',
};
/** Auth data extracted from Gemini CLI auth file */
interface GeminiCliAuthData {
@@ -90,6 +90,7 @@ interface GeminiCliSupplementaryInfo {
tierLabel: string | null;
tierId: string | null;
creditBalance: number | null;
normalizedTier: 'free' | 'pro' | 'ultra' | 'unknown';
}
/**
@@ -278,8 +279,7 @@ function resolveGeminiCliTierId(payload: GeminiCliCodeAssistResponse | null): st
function resolveGeminiCliTierLabel(payload: GeminiCliCodeAssistResponse | null): string | null {
const tierId = resolveGeminiCliTierId(payload);
if (!tierId) return null;
return GEMINI_CLI_TIER_LABELS[tierId] ?? tierId;
return getProviderTierLabel(tierId);
}
function resolveGeminiCliCreditBalance(payload: GeminiCliCodeAssistResponse | null): number | null {
@@ -340,7 +340,7 @@ async function fetchGeminiCliSupplementary(
if (verbose) {
console.error(`[i] Gemini CLI supplementary metadata unavailable: HTTP ${response.status}`);
}
return { tierLabel: null, tierId: null, creditBalance: null };
return { tierLabel: null, tierId: null, creditBalance: null, normalizedTier: 'unknown' };
}
const payload = (await response.json()) as GeminiCliCodeAssistResponse;
@@ -348,6 +348,7 @@ async function fetchGeminiCliSupplementary(
tierLabel: resolveGeminiCliTierLabel(payload),
tierId: resolveGeminiCliTierId(payload),
creditBalance: resolveGeminiCliCreditBalance(payload),
normalizedTier: normalizeProviderTierId(resolveGeminiCliTierId(payload)),
};
} catch (error) {
clearTimeout(timeoutId);
@@ -355,7 +356,7 @@ async function fetchGeminiCliSupplementary(
const message = error instanceof Error ? error.message : 'Unknown error';
console.error(`[i] Gemini CLI supplementary metadata skipped: ${message}`);
}
return { tierLabel: null, tierId: null, creditBalance: null };
return { tierLabel: null, tierId: null, creditBalance: null, normalizedTier: 'unknown' };
}
}
@@ -371,6 +372,7 @@ function buildGeminiCliFailureResult(
retryable?: boolean;
needsReauth?: boolean;
isForbidden?: boolean;
entitlement?: ProviderEntitlementEvidence;
}
): GeminiCliQuotaResult {
return {
@@ -390,6 +392,7 @@ function buildGeminiCliFailureResult(
retryable: options.retryable,
needsReauth: options.needsReauth,
isForbidden: options.isForbidden,
entitlement: options.entitlement,
};
}
@@ -549,10 +552,37 @@ function buildGeminiCliHttpFailureResult(
actionHint: buildGeminiCliForbiddenActionHint(parsed),
isForbidden: true,
retryable: false,
entitlement: buildProviderEntitlementEvidence({
normalizedTier: 'unknown',
source: 'runtime_inference',
confidence: 'medium',
accessState: 'not_entitled',
capacityState: 'unknown',
}),
});
}
if (status === 429) {
if (isModelCapacityExhausted(parsed.message, parsed.errorDetail, parsed.errorCode)) {
return buildGeminiCliFailureResult(accountId, projectId, {
error: parsed.message || 'Model capacity exhausted for this account right now',
httpStatus: 429,
errorCode: 'capacity_exhausted',
errorDetail: parsed.errorDetail,
actionHint:
'Retry later or switch to another Gemini model. This indicates temporary model capacity, not an authentication failure.',
retryable: true,
entitlement: buildProviderEntitlementEvidence({
normalizedTier: 'unknown',
source: 'runtime_inference',
confidence: 'medium',
accessState: 'entitled',
capacityState: 'capacity_exhausted',
notes: 'Upstream returned MODEL_CAPACITY_EXHAUSTED for this model.',
}),
});
}
return buildGeminiCliFailureResult(accountId, projectId, {
error: parsed.message || 'Rate limited - try again later',
httpStatus: 429,
@@ -560,6 +590,13 @@ function buildGeminiCliHttpFailureResult(
errorDetail: parsed.errorDetail,
actionHint: 'Retry after a short delay.',
retryable: true,
entitlement: buildProviderEntitlementEvidence({
normalizedTier: 'unknown',
source: 'runtime_inference',
confidence: 'low',
accessState: 'unknown',
capacityState: 'rate_limited',
}),
});
}
@@ -683,6 +720,10 @@ async function fetchWithAuthData(
if (verbose) console.error(`[i] Gemini CLI buckets found: ${buckets.length}`);
if (supplementary.normalizedTier !== 'unknown') {
setAccountTier('gemini', accountId, supplementary.normalizedTier);
}
return {
success: true,
buckets,
@@ -690,6 +731,15 @@ async function fetchWithAuthData(
tierLabel: supplementary.tierLabel,
tierId: supplementary.tierId,
creditBalance: supplementary.creditBalance,
entitlement: buildProviderEntitlementEvidence({
normalizedTier: supplementary.normalizedTier,
rawTierId: supplementary.tierId,
rawTierLabel: supplementary.tierLabel,
source: supplementary.tierId ? 'runtime_api' : 'runtime_inference',
confidence: supplementary.tierId ? 'high' : 'medium',
accessState: 'entitled',
capacityState: 'available',
}),
lastUpdated: Date.now(),
accountId,
};
+31 -18
View File
@@ -17,6 +17,12 @@ import {
type AccountTier,
} from './account-manager';
import { sanitizeEmail, isTokenExpired } from './auth-utils';
import {
buildProviderEntitlementEvidence,
getProviderTierLabel,
normalizeProviderTierId,
} from './provider-entitlement-evidence';
import type { ProviderEntitlementEvidence } from './provider-entitlement-types';
import { buildManagementHeaders, buildProxyUrl, getProxyTarget } from './proxy-target-resolver';
/** Individual model quota info */
@@ -67,6 +73,8 @@ export interface QuotaResult {
projectId?: string;
/** Detected account tier based on model access */
tier?: AccountTier;
/** Richer provider entitlement evidence derived from live/runtime signals */
entitlement?: ProviderEntitlementEvidence;
}
/** Google Cloud Code API endpoints */
@@ -175,6 +183,8 @@ interface ManagedResponse {
interface ProjectLookupResult {
projectId: string | null;
tier?: AccountTier;
rawTierId?: string | null;
rawTierLabel?: string | null;
error?: string;
errorCode?: string;
errorDetail?: string;
@@ -518,21 +528,6 @@ function readAuthData(provider: CLIProxyProvider, accountId: string): AuthData |
* API returns: "g1-ultra-tier", "g1-pro-tier", "standard-tier", etc.
* Priority: ultra > pro > free
*/
function mapTierString(tierStr: string | undefined): AccountTier {
if (!tierStr) return 'unknown';
const normalized = tierStr.toLowerCase();
// Match "g1-ultra-tier" or "ultra" anywhere in string
if (normalized.includes('ultra')) return 'ultra';
// Match "g1-pro-tier" or "pro" anywhere in string
if (normalized.includes('pro')) return 'pro';
// Match free/legacy tiers
if (normalized.includes('free') || normalized.includes('legacy')) {
return 'free';
}
// "standard-tier" and other unknown values = unknown
return 'unknown';
}
/**
* Get project ID and tier via loadCodeAssist endpoint
* Uses paidTier.id for accurate tier detection (g1-ultra-tier, g1-pro-tier)
@@ -590,10 +585,15 @@ async function getProjectId(accountId: string, accessToken: string): Promise<Pro
}
// Extract tier - paidTier reflects actual subscription status, takes priority
const tierStr = data.paidTier?.id || data.currentTier?.id;
const tier = mapTierString(tierStr);
const rawTierId = (data.paidTier?.id || data.currentTier?.id || '').trim() || null;
const tier = normalizeProviderTierId(rawTierId);
return { projectId: projectId.trim(), tier };
return {
projectId: projectId.trim(),
tier,
rawTierId,
rawTierLabel: getProviderTierLabel(rawTierId),
};
}
/**
@@ -727,6 +727,8 @@ export async function fetchAccountQuota(
// Get project ID and tier - prefer stored project ID, but always call API for tier
let projectId = authData.projectId;
let apiTier: AccountTier = 'unknown';
let rawTierId: string | null = null;
let rawTierLabel: string | null = null;
// Always call loadCodeAssist to get accurate tier from API.
// If the file token is stale, the helper retries through CLIProxy management auth.
@@ -755,6 +757,8 @@ export async function fetchAccountQuota(
// Use API project ID if available, else fallback to stored
projectId = lastProjectResult.projectId || projectId;
apiTier = lastProjectResult.tier || 'unknown';
rawTierId = lastProjectResult.rawTierId || null;
rawTierLabel = lastProjectResult.rawTierLabel || null;
if (verbose) console.error(`[i] Project ID: ${projectId || 'not found'}`);
@@ -769,6 +773,15 @@ export async function fetchAccountQuota(
if (result.success) {
const finalTier = apiTier !== 'unknown' ? apiTier : 'unknown';
result.tier = finalTier;
result.entitlement = buildProviderEntitlementEvidence({
normalizedTier: finalTier,
rawTierId,
rawTierLabel,
source: rawTierId ? 'runtime_api' : 'runtime_inference',
confidence: rawTierId ? 'high' : 'medium',
accessState: 'entitled',
capacityState: 'available',
});
if (finalTier !== 'unknown') {
setAccountTier(provider, accountId, finalTier);
}
+4
View File
@@ -5,6 +5,8 @@
* Supports Antigravity, Codex, Claude, Gemini CLI, and GitHub Copilot OAuth providers.
*/
import type { ProviderEntitlementEvidence } from './provider-entitlement-types';
/** Supported quota providers */
export type QuotaProvider = 'agy' | 'codex' | 'claude' | 'gemini' | 'ghcp';
@@ -200,6 +202,8 @@ export interface GeminiCliQuotaResult extends QuotaErrorMetadata {
tierId?: string | null;
/** Available Google One AI credits when reported by the API */
creditBalance?: number | null;
/** Richer provider entitlement evidence derived from live/runtime signals */
entitlement?: ProviderEntitlementEvidence;
/** Timestamp of fetch */
lastUpdated: number;
/** Error message if fetch failed */
+12 -1
View File
@@ -103,6 +103,13 @@ function formatCliAccountLabel(account: { id: string; email?: string; nickname?:
return account.nickname ? `${account.nickname} (${displayName})` : displayName;
}
function resolveDisplayedTier(
accountTier: string | undefined,
liveTier: string | undefined
): string {
return (liveTier && liveTier !== 'unknown' ? liveTier : accountTier) || 'unknown';
}
interface QuotaFailureDisplayEntry {
tone: 'error' | 'info' | 'dim';
text: string;
@@ -363,7 +370,7 @@ function displayAntigravityQuotaSection(
if (isOnCooldown(provider, account.id)) statusParts.push(color('COOLDOWN', 'warning'));
const defaultMark = account.isDefault ? color('*', 'success') : ' ';
const tier = account.tier || 'unknown';
const tier = resolveDisplayedTier(account.tier, quota?.entitlement?.normalizedTier);
const status = statusParts.join(', ');
rows.push([defaultMark, formatCliAccountLabel(account), tier, avgQuota, status]);
@@ -641,6 +648,9 @@ function displayGeminiCliQuotaSection(
if (quota.tierLabel) {
console.log(` Tier: ${dim(quota.tierLabel)}`);
}
if (quota.entitlement?.rawTierId) {
console.log(` Tier ID: ${dim(quota.entitlement.rawTierId)}`);
}
if (quota.creditBalance !== null && quota.creditBalance !== undefined) {
console.log(` Credits: ${dim(quota.creditBalance.toLocaleString())}`);
}
@@ -783,6 +793,7 @@ const QUOTA_PROVIDER_RUNTIME: Record<QuotaSupportedProvider, QuotaProviderRuntim
export const __testExports = {
getQuotaFailureDisplayEntries,
resolveDisplayedTier,
};
export async function handleQuotaStatus(
@@ -183,6 +183,16 @@ describe('Gemini CLI Quota Fetcher', () => {
]);
});
it('should keep Gemini 3.1 Flash Lite preview inside the Flash Lite family', () => {
const rawBuckets = [{ model_id: 'gemini-3.1-flash-lite-preview', remaining_fraction: 0.65 }];
const buckets = buildGeminiCliBuckets(rawBuckets);
expect(buckets).toHaveLength(1);
expect(buckets[0].label).toBe('Gemini Flash Lite Series');
expect(buckets[0].modelIds).toContain('gemini-3.1-flash-lite-preview');
});
it('should recognize Gemini 3.1 preview IDs during the rollout', () => {
const rawBuckets = [
{ model_id: 'gemini-3.1-flash-preview', remaining_fraction: 0.7 },
@@ -403,6 +413,13 @@ describe('Gemini CLI Quota Fetcher', () => {
expect(result.tierLabel).toBe('Pro');
expect(result.tierId).toBe('g1-pro-tier');
expect(result.creditBalance).toBe(12);
expect(result.entitlement).toMatchObject({
normalizedTier: 'pro',
rawTierId: 'g1-pro-tier',
rawTierLabel: 'Pro',
accessState: 'entitled',
capacityState: 'available',
});
expect(result.buckets.map((bucket) => bucket.label)).toEqual([
'Gemini Flash Lite Series',
'Gemini Flash Series',
@@ -822,6 +839,43 @@ describe('Gemini CLI Quota Fetcher', () => {
globalThis.fetch = originalFetch;
}
});
it('classifies model capacity exhaustion separately from generic rate limits', async () => {
writeActiveGeminiAccount('capacity@example.com');
mockFetch([
{
url: GEMINI_QUOTA_URL,
method: 'POST',
status: 429,
response: {
error: {
code: 429,
message: 'No capacity available for model gemini-3.1-pro-preview on the server',
status: 'RESOURCE_EXHAUSTED',
details: [
{
'@type': 'type.googleapis.com/google.rpc.ErrorInfo',
reason: 'MODEL_CAPACITY_EXHAUSTED',
metadata: { model: 'gemini-3.1-pro-preview' },
},
],
},
},
},
]);
const result = await fetchGeminiCliQuota('capacity@example.com');
expect(result.success).toBe(false);
expect(result.httpStatus).toBe(429);
expect(result.errorCode).toBe('capacity_exhausted');
expect(result.retryable).toBe(true);
expect(result.entitlement).toMatchObject({
accessState: 'entitled',
capacityState: 'capacity_exhausted',
});
});
});
describe('direct Gemini error helper coverage', () => {
@@ -75,4 +75,12 @@ describe('cliproxy quota subcommand failure formatting', () => {
},
]);
});
it('prefers live quota tier over stale account tier', async () => {
const { resolveDisplayedTier } = await loadQuotaCommandTestExports();
expect(resolveDisplayedTier('unknown', 'pro')).toBe('pro');
expect(resolveDisplayedTier('pro', 'ultra')).toBe('ultra');
expect(resolveDisplayedTier('pro', 'unknown')).toBe('pro');
});
});
@@ -86,7 +86,9 @@ export function AccountSurfaceCard({
const title = displayEmail || identity.email || accountId;
const normalizedProvider = provider.toLowerCase();
const showTierBadge =
(normalizedProvider === 'agy' || normalizedProvider === 'antigravity') &&
(normalizedProvider === 'agy' ||
normalizedProvider === 'antigravity' ||
normalizedProvider === 'gemini') &&
tier &&
tier !== 'unknown' &&
tier !== 'free';
@@ -21,6 +21,7 @@ import {
type ModelTier,
type UnifiedQuotaResult,
} from '@/lib/utils';
import type { ProviderEntitlementEvidence } from '@/lib/api-client';
interface QuotaTooltipContentProps {
quota: UnifiedQuotaResult | null | undefined;
@@ -77,6 +78,35 @@ function getClaudeWindowDisplayLabel(rateLimitType: string, fallback: string): s
}
}
function renderEntitlementRows(entitlement: ProviderEntitlementEvidence | undefined) {
if (!entitlement) return null;
const rows: Array<{ label: string; value: string | null }> = [];
if (entitlement.rawTierLabel) {
rows.push({ label: 'Tier', value: entitlement.rawTierLabel });
} else if (entitlement.normalizedTier !== 'unknown') {
rows.push({ label: 'Tier', value: entitlement.normalizedTier });
}
if (entitlement.rawTierId) {
rows.push({ label: 'Tier ID', value: entitlement.rawTierId });
}
if (entitlement.accessState !== 'entitled' || entitlement.capacityState !== 'available') {
rows.push({
label: 'State',
value: `${entitlement.accessState.replaceAll('_', ' ')} / ${entitlement.capacityState.replaceAll('_', ' ')}`,
});
}
if (rows.length === 0) return null;
return rows.map((row) => (
<div key={row.label} className="flex justify-between gap-4">
<span className="text-muted-foreground">{row.label}</span>
<span className="font-mono">{row.value}</span>
</div>
));
}
/**
* Renders provider-specific quota tooltip content
* Uses type guards for proper TypeScript narrowing
@@ -132,6 +162,7 @@ export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentPro
return (
<div className="text-xs space-y-1.5">
{renderEntitlementRows(quota.entitlement)}
<p className="font-medium">Model Quotas:</p>
{tierOrder.map((tier, idx) => {
const models = groups.get(tier);
@@ -268,10 +299,13 @@ export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentPro
// Gemini provider tooltip
if (isGeminiQuotaResult(quota)) {
const hasBucketResetTime = quota.buckets.some((bucket) => !!bucket.resetTime);
const hasEntitlementTier =
!!quota.entitlement?.rawTierLabel || quota.entitlement?.normalizedTier !== 'unknown';
return (
<div className="text-xs space-y-1.5">
{quota.tierLabel && (
{renderEntitlementRows(quota.entitlement)}
{!hasEntitlementTier && quota.tierLabel && (
<div className="flex justify-between gap-4">
<span className="text-muted-foreground">Tier</span>
<span className="font-mono">{quota.tierLabel}</span>
+26
View File
@@ -562,6 +562,30 @@ export interface QuotaResult {
retryable?: boolean;
/** True if token is expired and needs re-authentication */
needsReauth?: boolean;
/** Richer provider entitlement evidence derived from live/runtime signals */
entitlement?: ProviderEntitlementEvidence;
}
export interface ProviderEntitlementEvidence {
normalizedTier: 'free' | 'pro' | 'ultra' | 'unknown';
rawTierId: string | null;
rawTierLabel: string | null;
source: 'runtime_api' | 'runtime_inference' | 'registry_cache' | 'official_docs';
confidence: 'high' | 'medium' | 'low';
accessState:
| 'entitled'
| 'not_entitled'
| 'capacity_exhausted'
| 'temporarily_unavailable'
| 'unknown';
capacityState:
| 'available'
| 'capacity_exhausted'
| 'rate_limited'
| 'temporarily_unavailable'
| 'unknown';
lastVerifiedAt: number;
notes?: string | null;
}
/** Codex rate limit window */
@@ -725,6 +749,8 @@ export interface GeminiCliQuotaResult {
tierId?: string | null;
/** Available Google One AI credits when reported by the API */
creditBalance?: number | null;
/** Richer provider entitlement evidence derived from live/runtime signals */
entitlement?: ProviderEntitlementEvidence;
/** Timestamp of fetch */
lastUpdated: number;
/** Upstream HTTP status when available */
+20
View File
@@ -771,6 +771,25 @@ export function getQuotaFailureInfo(
const technicalDetail = buildQuotaTechnicalDetail(quota);
const rawDetail = buildQuotaRawDetail(quota, summary, technicalDetail);
const lowerSummary = summary.toLowerCase();
const entitlement = 'entitlement' in quota ? quota.entitlement : undefined;
if (
quota.errorCode === 'capacity_exhausted' ||
entitlement?.capacityState === 'capacity_exhausted' ||
lowerSummary.includes('no capacity available') ||
lowerSummary.includes('capacity exhausted')
) {
return {
label: 'Capacity',
summary,
actionHint:
actionHint ||
'Retry later or switch to another model. This is a temporary provider capacity issue.',
technicalDetail,
rawDetail,
tone: 'warning',
};
}
if (
quota.needsReauth ||
@@ -808,6 +827,7 @@ export function getQuotaFailureInfo(
}
if (
entitlement?.accessState === 'not_entitled' ||
quota.isForbidden ||
quota.httpStatus === 403 ||
errorCode === 'quota_api_forbidden' ||
@@ -34,6 +34,17 @@ function createGeminiQuotaResult(
tierLabel: 'Pro',
tierId: 'g1-pro-tier',
creditBalance: 12,
entitlement: {
normalizedTier: 'pro',
rawTierId: 'g1-pro-tier',
rawTierLabel: 'Pro',
source: 'runtime_api',
confidence: 'high',
accessState: 'entitled',
capacityState: 'available',
lastVerifiedAt: Date.now(),
notes: null,
},
lastUpdated: Date.now(),
...overrides,
};
@@ -54,6 +65,8 @@ describe('QuotaTooltipContent', () => {
expect(screen.getByText('Tier')).toBeInTheDocument();
expect(screen.getByText('Pro')).toBeInTheDocument();
expect(screen.getByText('Tier ID')).toBeInTheDocument();
expect(screen.getByText('g1-pro-tier')).toBeInTheDocument();
expect(screen.getByText('Credits')).toBeInTheDocument();
expect(screen.getByText('12')).toBeInTheDocument();
expect(screen.getByText('Gemini Flash Lite Series')).toBeInTheDocument();