diff --git a/src/cliproxy/account-manager.ts b/src/cliproxy/account-manager.ts index 57495035..b1044cdc 100644 --- a/src/cliproxy/account-manager.ts +++ b/src/cliproxy/account-manager.ts @@ -16,8 +16,8 @@ import { CLIPROXY_PROFILES } from '../auth/profile-detector'; import { getCliproxyDir, getAuthDir } from './config-generator'; import { PROVIDER_TYPE_VALUES } from './auth/auth-types'; -/** Account tier for quota management (free vs paid - no Pro/Ultra distinction needed) */ -export type AccountTier = 'free' | 'paid' | 'unknown'; +/** Account tier for quota management: ultra > pro > free */ +export type AccountTier = 'free' | 'pro' | 'ultra' | 'unknown'; /** * Providers that typically have empty email in OAuth token files. @@ -47,7 +47,7 @@ export interface AccountInfo { paused?: boolean; /** ISO timestamp when paused */ pausedAt?: string; - /** Account tier: free or paid (Pro/Ultra combined) */ + /** Account tier: ultra, pro, or free */ tier?: AccountTier; /** GCP Project ID (Antigravity only) - read-only, fetched from auth token */ projectId?: string; diff --git a/src/cliproxy/model-catalog.ts b/src/cliproxy/model-catalog.ts index 7f035350..80406bb9 100644 --- a/src/cliproxy/model-catalog.ts +++ b/src/cliproxy/model-catalog.ts @@ -36,8 +36,8 @@ export interface ModelEntry { id: string; /** Human-readable name for display */ name: string; - /** Access tier indicator - 'paid' means requires paid Google account (not free tier) */ - tier?: 'free' | 'paid'; + /** Access tier indicator - 'ultra' for Claude, 'pro' for premium Gemini, 'free' for basic */ + tier?: 'free' | 'pro' | 'ultra'; /** Optional description for the model */ description?: string; /** Model has known issues - show warning when selected */ @@ -119,7 +119,7 @@ export const MODEL_CATALOG: Partial> = { id: 'gemini-3-pro-preview', name: 'Gemini 3 Pro', - tier: 'paid', + tier: 'pro', description: 'Latest model, requires paid Google account', thinking: { type: 'levels', levels: ['low', 'high'], dynamicAllowed: true }, }, diff --git a/src/cliproxy/model-config.ts b/src/cliproxy/model-config.ts index ddec180b..2da1cc63 100644 --- a/src/cliproxy/model-config.ts +++ b/src/cliproxy/model-config.ts @@ -51,8 +51,13 @@ export function getCurrentModel( * Format model entry for display in selection list */ function formatModelOption(model: ModelEntry): string { - // Tier badge: clarify that "paid" means paid Google account (not free tier) - const tierBadge = model.tier === 'paid' ? color(' [Paid Tier]', 'warning') : ''; + // Tier badge: ultra/pro indicate paid tiers + const tierBadge = + model.tier === 'ultra' + ? color(' [Ultra]', 'warning') + : model.tier === 'pro' + ? color(' [Pro]', 'warning') + : ''; const brokenBadge = model.broken ? color(' [BROKEN]', 'error') : ''; const deprecatedBadge = model.deprecated ? color(' [DEPRECATED]', 'warning') : ''; return `${model.name}${tierBadge}${brokenBadge}${deprecatedBadge}`; @@ -64,7 +69,12 @@ function formatModelOption(model: ModelEntry): string { function formatModelDetailed(model: ModelEntry, isCurrent: boolean): string { const marker = isCurrent ? color('>', 'success') : ' '; const name = isCurrent ? bold(model.name) : model.name; - const tierBadge = model.tier === 'paid' ? color(' [Paid Tier]', 'warning') : ''; + const tierBadge = + model.tier === 'ultra' + ? color(' [Ultra]', 'warning') + : model.tier === 'pro' + ? color(' [Pro]', 'warning') + : ''; const brokenBadge = model.broken ? color(' [BROKEN]', 'error') : ''; const deprecatedBadge = model.deprecated ? color(' [DEPRECATED]', 'warning') : ''; const desc = model.description ? dim(` - ${model.description}`) : ''; diff --git a/src/cliproxy/quota-fetcher.ts b/src/cliproxy/quota-fetcher.ts index 9301d985..e9e7fd81 100644 --- a/src/cliproxy/quota-fetcher.ts +++ b/src/cliproxy/quota-fetcher.ts @@ -303,48 +303,27 @@ function readAuthData(provider: CLIProxyProvider, accountId: string): AuthData | /** * Map tier ID string to AccountTier type - * Simplified: anything with 'pro' or 'ultra' = paid, 'free'/'legacy' = free + * 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(); - if (normalized.includes('ultra') || normalized.includes('pro')) return 'paid'; + // 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 should NOT map to 'free' - // Let inferTierFromModels handle the detection + // "standard-tier" and other unknown values = unknown return 'unknown'; } -/** - * Infer tier from model access patterns. - * - Paid: Has access to Claude models OR premium Gemini models - * - Free: Only basic models - * - * Claude models are Ultra-exclusive, premium Gemini indicates Pro/Ultra. - * Both are "paid" tier for our purposes. - */ -function inferTierFromModels(models: ModelQuota[]): AccountTier { - if (models.length === 0) return 'unknown'; - - // Check for Claude models (Ultra-exclusive) or premium Gemini (Pro/Ultra) - const hasPaidAccess = models.some((m) => { - const name = m.name.toLowerCase(); - return ( - name.includes('claude') || - name.includes('gemini-3-pro') || - name.includes('gemini-2.5-pro') || - name.includes('gemini-pro-high') - ); - }); - - return hasPaidAccess ? 'paid' : 'unknown'; -} - /** * Get project ID and tier via loadCodeAssist endpoint - * Uses allowedTiers array with isDefault=true (CLIProxyAPIPlus approach) + * Uses paidTier.id for accurate tier detection (g1-ultra-tier, g1-pro-tier) */ async function getProjectId(accessToken: string): Promise<{ projectId: string | null; @@ -406,17 +385,10 @@ async function getProjectId(accessToken: string): Promise<{ }; } - // Extract tier using CLIProxyAPIPlus approach: - // 1. Find tier with isDefault=true in allowedTiers array - // 2. Fallback to paidTier > currentTier - let tierStr: string | undefined; - if (data.allowedTiers && Array.isArray(data.allowedTiers)) { - const defaultTier = data.allowedTiers.find((t) => t.isDefault); - tierStr = defaultTier?.id; - } - if (!tierStr) { - tierStr = data.paidTier?.id || data.currentTier?.id; - } + // Extract tier - paidTier reflects actual subscription status, takes priority + // API returns: paidTier.id = "g1-ultra-tier" or "g1-pro-tier" + // allowedTiers/currentTier often return "standard-tier" which is not useful + const tierStr = data.paidTier?.id || data.currentTier?.id; const tier = mapTierString(tierStr); return { projectId: projectId.trim(), tier }; @@ -650,15 +622,14 @@ export async function fetchAccountQuota( refreshResult.accessToken, projectId as string ); - // Determine tier: model access (Claude = Ultra) > API tier > fallback + // Determine tier from API response only (model inference is unreliable) if (retryResult.success) { - let finalTier = inferTierFromModels(retryResult.models); - if (finalTier === 'unknown') { - finalTier = apiTier !== 'unknown' ? apiTier : 'paid'; - } + const finalTier = apiTier !== 'unknown' ? apiTier : 'unknown'; retryResult.tier = finalTier; retryResult.accountId = accountId; - setAccountTier(provider, accountId, finalTier); + if (finalTier !== 'unknown') { + setAccountTier(provider, accountId, finalTier); + } if (verbose && retryResult.error) { console.log(`[!] Error: ${retryResult.error}`); } @@ -667,15 +638,14 @@ export async function fetchAccountQuota( } } - // Determine tier: model access > API tier > fallback to paid + // Determine tier from API response only if (result.success) { - let finalTier = inferTierFromModels(result.models); - if (finalTier === 'unknown') { - finalTier = apiTier !== 'unknown' ? apiTier : 'paid'; - } + const finalTier = apiTier !== 'unknown' ? apiTier : 'unknown'; result.tier = finalTier; result.accountId = accountId; - setAccountTier(provider, accountId, finalTier); + if (finalTier !== 'unknown') { + setAccountTier(provider, accountId, finalTier); + } } if (verbose && result.error) { diff --git a/src/cliproxy/quota-manager.ts b/src/cliproxy/quota-manager.ts index 48e1bc12..a5943c6a 100644 --- a/src/cliproxy/quota-manager.ts +++ b/src/cliproxy/quota-manager.ts @@ -200,7 +200,7 @@ export async function findHealthyAccount( exclude: string[] ): Promise<{ id: string; tier: string; lastQuota: number } | null> { const config = loadOrCreateUnifiedConfig(); - const tierPriority = config.quota_management?.auto?.tier_priority ?? ['paid']; + const tierPriority = config.quota_management?.auto?.tier_priority ?? ['ultra', 'pro', 'free']; const threshold = config.quota_management?.auto?.exhaustion_threshold ?? 5; const accounts = getProviderAccounts(provider); @@ -225,7 +225,7 @@ export async function findHealthyAccount( return { id: account.id, - tier: account.tier || 'paid', + tier: account.tier || 'unknown', lastQuota: avgQuota, }; }) diff --git a/src/commands/cliproxy-command.ts b/src/commands/cliproxy-command.ts index 55761a59..3e94af8d 100644 --- a/src/commands/cliproxy-command.ts +++ b/src/commands/cliproxy-command.ts @@ -152,7 +152,12 @@ function parseProfileArgs(args: string[]): CliproxyProfileArgs { } function formatModelOption(model: ModelEntry): string { - const tierBadge = model.tier === 'paid' ? color(' [Paid Tier]', 'warning') : ''; + const tierBadge = + model.tier === 'ultra' + ? color(' [Ultra]', 'warning') + : model.tier === 'pro' + ? color(' [Pro]', 'warning') + : ''; return `${model.name}${tierBadge}`; } diff --git a/src/config/unified-config-types.ts b/src/config/unified-config-types.ts index 130d6e0b..cf7e0f1f 100644 --- a/src/config/unified-config-types.ts +++ b/src/config/unified-config-types.ts @@ -409,7 +409,7 @@ export interface QuotaManagementConfig { export const DEFAULT_AUTO_QUOTA_CONFIG: AutoQuotaConfig = { preflight_check: true, exhaustion_threshold: 5, - tier_priority: ['paid'], + tier_priority: ['ultra', 'pro', 'free'], cooldown_minutes: 5, }; diff --git a/tests/unit/cliproxy/model-catalog.test.js b/tests/unit/cliproxy/model-catalog.test.js index 0cd8beed..8b540bca 100644 --- a/tests/unit/cliproxy/model-catalog.test.js +++ b/tests/unit/cliproxy/model-catalog.test.js @@ -88,12 +88,12 @@ describe('Model Catalog', () => { assert.strictEqual(MODEL_CATALOG.gemini.defaultModel, 'gemini-2.5-pro'); }); - it('includes Gemini 3 Pro with paid tier', () => { + it('includes Gemini 3 Pro with pro tier', () => { const { MODEL_CATALOG } = modelCatalog; const gem3 = MODEL_CATALOG.gemini.models.find((m) => m.id === 'gemini-3-pro-preview'); assert(gem3, 'Should include Gemini 3 Pro'); assert.strictEqual(gem3.name, 'Gemini 3 Pro'); - assert.strictEqual(gem3.tier, 'paid'); + assert.strictEqual(gem3.tier, 'pro'); }); it('includes Gemini 2.5 Pro without tier (free)', () => { @@ -197,7 +197,7 @@ describe('Model Catalog', () => { assert(typeof model.name === 'string', `Model name should be string`); // tier is optional if (model.tier !== undefined) { - assert(['free', 'paid'].includes(model.tier), `Invalid tier: ${model.tier}`); + assert(['free', 'pro', 'ultra'].includes(model.tier), `Invalid tier: ${model.tier}`); } } } diff --git a/ui/src/components/account/flow-viz/account-card.tsx b/ui/src/components/account/flow-viz/account-card.tsx index 0b957e3b..110d6f8a 100644 --- a/ui/src/components/account/flow-viz/account-card.tsx +++ b/ui/src/components/account/flow-viz/account-card.tsx @@ -99,6 +99,10 @@ export function AccountCard({ // Show minimum quota of Claude models (primary), fallback to min of all models const minQuota = quota?.success ? getMinClaudeQuota(quota.models) : null; + // Tier badge (AGY only) - show P for Pro, U for Ultra + const showTierBadge = + isAgy && account.tier && account.tier !== 'unknown' && account.tier !== 'free'; + return (
- -
- + {/* Email with tier badge inline */} +
+ + {cleanEmail(account.email)} + + {showTierBadge && ( + + {account.tier} + )} - > - {cleanEmail(account.email)} - +
+ + {/* Pause/Resume button */} {onPauseToggle && ( @@ -167,7 +188,11 @@ export function AccountCard({ )} + + {/* Drag handle */} +
+ -
-
+
+
{/* Selection checkbox for bulk actions */} {selectable && ( diff --git a/ui/src/components/monitoring/auth-monitor/hooks.ts b/ui/src/components/monitoring/auth-monitor/hooks.ts index 4af76b84..14868be5 100644 --- a/ui/src/components/monitoring/auth-monitor/hooks.ts +++ b/ui/src/components/monitoring/auth-monitor/hooks.ts @@ -101,6 +101,7 @@ export function useAuthMonitorData(): AuthMonitorData { color: ACCOUNT_COLORS[colorIndex % ACCOUNT_COLORS.length], projectId: account.projectId, paused: account.paused, + tier: account.tier, }; accountsList.push(row); providerData.accounts.push(row); diff --git a/ui/src/components/monitoring/auth-monitor/types.ts b/ui/src/components/monitoring/auth-monitor/types.ts index 1490c134..1ddbf44a 100644 --- a/ui/src/components/monitoring/auth-monitor/types.ts +++ b/ui/src/components/monitoring/auth-monitor/types.ts @@ -2,6 +2,9 @@ * Type definitions for Auth Monitor components */ +/** Account tier for subscription level */ +export type AccountTier = 'free' | 'pro' | 'ultra' | 'unknown'; + export interface AccountRow { id: string; email: string; @@ -16,6 +19,8 @@ export interface AccountRow { projectId?: string; /** Whether account is paused (skipped in quota rotation) */ paused?: boolean; + /** Account tier (Antigravity only) */ + tier?: AccountTier; } export interface ProviderStats { diff --git a/ui/src/lib/api-client.ts b/ui/src/lib/api-client.ts index 77b73065..cf190caa 100644 --- a/ui/src/lib/api-client.ts +++ b/ui/src/lib/api-client.ts @@ -83,8 +83,8 @@ export interface OAuthAccount { paused?: boolean; /** ISO timestamp when account was paused */ pausedAt?: string; - /** Account tier: free or paid (Pro/Ultra combined) */ - tier?: 'free' | 'paid' | 'unknown'; + /** Account tier: free, pro, ultra, or unknown */ + tier?: 'free' | 'pro' | 'ultra' | 'unknown'; /** GCP Project ID (Antigravity only) - read-only */ projectId?: string; }