diff --git a/src/cliproxy/gemini-cli-quota-normalizer.ts b/src/cliproxy/gemini-cli-quota-normalizer.ts index 1d5425b9..3fad0de9 100644 --- a/src/cliproxy/gemini-cli-quota-normalizer.ts +++ b/src/cliproxy/gemini-cli-quota-normalizer.ts @@ -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', diff --git a/src/cliproxy/provider-entitlement-evidence.ts b/src/cliproxy/provider-entitlement-evidence.ts new file mode 100644 index 00000000..9b36576e --- /dev/null +++ b/src/cliproxy/provider-entitlement-evidence.ts @@ -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 = { + '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') + ); +} diff --git a/src/cliproxy/provider-entitlement-types.ts b/src/cliproxy/provider-entitlement-types.ts new file mode 100644 index 00000000..ef07b8db --- /dev/null +++ b/src/cliproxy/provider-entitlement-types.ts @@ -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; +} diff --git a/src/cliproxy/quota-fetcher-gemini-cli.ts b/src/cliproxy/quota-fetcher-gemini-cli.ts index 1dc8fe7a..27d80d6a 100644 --- a/src/cliproxy/quota-fetcher-gemini-cli.ts +++ b/src/cliproxy/quota-fetcher-gemini-cli.ts @@ -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 = { - '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, }; diff --git a/src/cliproxy/quota-fetcher.ts b/src/cliproxy/quota-fetcher.ts index 1365ae42..06765c58 100644 --- a/src/cliproxy/quota-fetcher.ts +++ b/src/cliproxy/quota-fetcher.ts @@ -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 { ]); }); + 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', () => { diff --git a/tests/unit/commands/cliproxy-quota-subcommand.test.ts b/tests/unit/commands/cliproxy-quota-subcommand.test.ts index 0b49cfef..f6bd033d 100644 --- a/tests/unit/commands/cliproxy-quota-subcommand.test.ts +++ b/tests/unit/commands/cliproxy-quota-subcommand.test.ts @@ -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'); + }); }); diff --git a/ui/src/components/account/shared/account-surface-card.tsx b/ui/src/components/account/shared/account-surface-card.tsx index 64a1795c..3d91d71f 100644 --- a/ui/src/components/account/shared/account-surface-card.tsx +++ b/ui/src/components/account/shared/account-surface-card.tsx @@ -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'; diff --git a/ui/src/components/shared/quota-tooltip-content.tsx b/ui/src/components/shared/quota-tooltip-content.tsx index 5dcda088..bd1e14dd 100644 --- a/ui/src/components/shared/quota-tooltip-content.tsx +++ b/ui/src/components/shared/quota-tooltip-content.tsx @@ -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) => ( +
+ {row.label} + {row.value} +
+ )); +} + /** * Renders provider-specific quota tooltip content * Uses type guards for proper TypeScript narrowing @@ -132,6 +162,7 @@ export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentPro return (
+ {renderEntitlementRows(quota.entitlement)}

Model Quotas:

{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 (
- {quota.tierLabel && ( + {renderEntitlementRows(quota.entitlement)} + {!hasEntitlementTier && quota.tierLabel && (
Tier {quota.tierLabel} diff --git a/ui/src/lib/api-client.ts b/ui/src/lib/api-client.ts index 74a848ee..6a0e1bc4 100644 --- a/ui/src/lib/api-client.ts +++ b/ui/src/lib/api-client.ts @@ -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 */ diff --git a/ui/src/lib/utils.ts b/ui/src/lib/utils.ts index 9db71545..0105d1e1 100644 --- a/ui/src/lib/utils.ts +++ b/ui/src/lib/utils.ts @@ -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' || diff --git a/ui/tests/unit/ui/components/shared/quota-tooltip-content.test.tsx b/ui/tests/unit/ui/components/shared/quota-tooltip-content.test.tsx index bada72b1..790ec8a1 100644 --- a/ui/tests/unit/ui/components/shared/quota-tooltip-content.test.tsx +++ b/ui/tests/unit/ui/components/shared/quota-tooltip-content.test.tsx @@ -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();