From aeb9abc998a5114007e34c478cf5c79213ea1fe7 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Wed, 28 Jan 2026 15:11:00 -0500 Subject: [PATCH 1/6] feat(cliproxy): add granular account tier prioritization (ultra/pro/free) - change AccountTier from binary (free/paid) to granular (ultra/pro/free/unknown) - update mapTierString() to parse API tier strings correctly - remove faulty inferTierFromModels() fallback - tier comes only from API - update default tier_priority config to ['ultra', 'pro', 'free'] - update model catalog tier types and display logic Closes #387 --- src/cliproxy/account-manager.ts | 6 +-- src/cliproxy/model-catalog.ts | 6 +-- src/cliproxy/model-config.ts | 16 +++++-- src/cliproxy/quota-fetcher.ts | 52 ++++++----------------- src/cliproxy/quota-manager.ts | 4 +- src/commands/cliproxy-command.ts | 7 ++- src/config/unified-config-types.ts | 2 +- tests/unit/cliproxy/model-catalog.test.js | 6 +-- 8 files changed, 44 insertions(+), 55 deletions(-) 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..f65f9540 100644 --- a/src/cliproxy/quota-fetcher.ts +++ b/src/cliproxy/quota-fetcher.ts @@ -303,12 +303,13 @@ function readAuthData(provider: CLIProxyProvider, accountId: string): AuthData | /** * Map tier ID string to AccountTier type - * Simplified: anything with 'pro' or 'ultra' = paid, 'free'/'legacy' = free + * 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'; + if (normalized.includes('ultra')) return 'ultra'; + if (normalized.includes('pro')) return 'pro'; if (normalized.includes('free') || normalized.includes('legacy')) { return 'free'; } @@ -317,31 +318,6 @@ function mapTierString(tierStr: string | undefined): AccountTier { 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) @@ -650,15 +626,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 +642,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..577052fc 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 || 'pro', 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}`); } } } From 87cced81952c168eaa4c6fd98059f089d765bcb3 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Wed, 28 Jan 2026 15:11:17 -0500 Subject: [PATCH 2/6] fix(ui): fix account card layout overflow - add min-w-0 flex-1 to left content container for proper shrinking - add gap-2 between content and dropdown menu - add shrink-0 to dropdown button to prevent overflow --- .../components/cliproxy/provider-editor/account-item.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/src/components/cliproxy/provider-editor/account-item.tsx b/ui/src/components/cliproxy/provider-editor/account-item.tsx index 0fa29685..269e4e8d 100644 --- a/ui/src/components/cliproxy/provider-editor/account-item.tsx +++ b/ui/src/components/cliproxy/provider-editor/account-item.tsx @@ -128,14 +128,14 @@ export function AccountItem({ return (
-
-
+
+
{/* Selection checkbox for bulk actions */} {selectable && ( From de23029b572c9e1db1c8a004ec214ce5a465570a Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Wed, 28 Jan 2026 15:17:23 -0500 Subject: [PATCH 3/6] fix(cliproxy): use paidTier for account tier detection instead of allowedTiers - paidTier.id contains actual subscription: g1-ultra-tier or g1-pro-tier - allowedTiers[isDefault] returns standard-tier for all accounts (not useful) - mapTierString now correctly parses g1-ultra-tier and g1-pro-tier formats --- src/cliproxy/quota-fetcher.ts | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/cliproxy/quota-fetcher.ts b/src/cliproxy/quota-fetcher.ts index f65f9540..960076c6 100644 --- a/src/cliproxy/quota-fetcher.ts +++ b/src/cliproxy/quota-fetcher.ts @@ -303,18 +303,21 @@ function readAuthData(provider: CLIProxyProvider, accountId: string): AuthData | /** * Map tier ID string to AccountTier type + * 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 should NOT map to 'free' - // Let inferTierFromModels handle the detection + // "standard-tier" and other unknown values = unknown return 'unknown'; } @@ -382,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 }; From 890fd140f2ede728f212ed0bfc77bc8609b3a09b Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Wed, 28 Jan 2026 16:30:41 -0500 Subject: [PATCH 4/6] feat(ui): add granular account tier types (pro/ultra/free) - update AccountTier from binary to granular - add tier field to AccountRow and AccountData types - pass tier through hooks data flow --- ui/src/components/account/flow-viz/types.ts | 5 +++++ ui/src/components/monitoring/auth-monitor/hooks.ts | 1 + ui/src/components/monitoring/auth-monitor/types.ts | 5 +++++ ui/src/lib/api-client.ts | 4 ++-- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/ui/src/components/account/flow-viz/types.ts b/ui/src/components/account/flow-viz/types.ts index 423155b1..a17e3e37 100644 --- a/ui/src/components/account/flow-viz/types.ts +++ b/ui/src/components/account/flow-viz/types.ts @@ -2,6 +2,9 @@ * Type definitions for Account Flow Visualization */ +/** Account tier for subscription level */ +export type AccountTier = 'free' | 'pro' | 'ultra' | 'unknown'; + /** Position offset for draggable cards */ export interface DragOffset { x: number; @@ -17,6 +20,8 @@ export interface AccountData { lastUsedAt?: string; color: string; paused?: boolean; + /** Account tier (Antigravity only) */ + tier?: AccountTier; } export interface ProviderData { 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; } From 31a91f609fad4af17f0e4bd3b0f0dbabe8bf2b77 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Wed, 28 Jan 2026 16:31:16 -0500 Subject: [PATCH 5/6] feat(ui): add tier badges to account cards - flow-viz: inline tier badge (ULTRA/PRO) with violet/yellow colors - provider-editor: tier badge on avatar corner (U/P) - subtle design with low opacity backgrounds --- .../account/flow-viz/account-card.tsx | 43 +++++++++++++++---- .../cliproxy/provider-editor/account-item.tsx | 41 ++++++++++-------- 2 files changed, 57 insertions(+), 27 deletions(-) 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 */} +
+ )} -
+
+ +
+ {/* Tier badge - fixed position on avatar */} + {account.tier && account.tier !== 'unknown' && account.tier !== 'free' && ( + + {account.tier === 'ultra' ? 'U' : 'P'} + )} - > -
@@ -200,18 +217,6 @@ export function AccountItem({ Default )} - {account.tier && account.tier !== 'unknown' && ( - - {account.tier} - - )} {account.paused && ( Date: Wed, 28 Jan 2026 16:41:27 -0500 Subject: [PATCH 6/6] fix(cliproxy): address PR review feedback - align tier fallback to 'unknown' instead of 'pro' in quota-manager - update stale comment referencing old allowedTiers approach --- src/cliproxy/quota-fetcher.ts | 2 +- src/cliproxy/quota-manager.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cliproxy/quota-fetcher.ts b/src/cliproxy/quota-fetcher.ts index 960076c6..e9e7fd81 100644 --- a/src/cliproxy/quota-fetcher.ts +++ b/src/cliproxy/quota-fetcher.ts @@ -323,7 +323,7 @@ function mapTierString(tierStr: string | undefined): AccountTier { /** * 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; diff --git a/src/cliproxy/quota-manager.ts b/src/cliproxy/quota-manager.ts index 577052fc..a5943c6a 100644 --- a/src/cliproxy/quota-manager.ts +++ b/src/cliproxy/quota-manager.ts @@ -225,7 +225,7 @@ export async function findHealthyAccount( return { id: account.id, - tier: account.tier || 'pro', + tier: account.tier || 'unknown', lastQuota: avgQuota, }; })