diff --git a/ui/bun.lock b/ui/bun.lock index c2e29de1..2756da98 100644 --- a/ui/bun.lock +++ b/ui/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "ui", diff --git a/ui/src/components/account/flow-viz/account-card.tsx b/ui/src/components/account/flow-viz/account-card.tsx index 110d6f8a..c5773214 100644 --- a/ui/src/components/account/flow-viz/account-card.tsx +++ b/ui/src/components/account/flow-viz/account-card.tsx @@ -9,11 +9,16 @@ import { getMinClaudeQuota, getModelsWithTiers, groupModelsByTier, + getMinCodexQuota, + getMinGeminiQuota, + getCodexResetTime, + getGeminiResetTime, type ModelTier, } from '@/lib/utils'; import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context'; import { GripVertical, Loader2, Clock, Pause, Play } from 'lucide-react'; import { useAccountQuota } from '@/hooks/use-cliproxy-stats'; +import type { CodexQuotaResult, GeminiCliQuotaResult, QuotaResult } from '@/lib/api-client'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Button } from '@/components/ui/button'; @@ -89,19 +94,36 @@ export function AccountCard({ const borderColor = getBorderColorStyle(zone, account.color); const connectorPosition = CONNECTOR_POSITION_MAP[zone]; - // Quota for AGY accounts - const isAgy = account.provider === 'agy'; + // Quota for CLIProxy accounts (agy, codex, gemini) + const isCliproxyProvider = ['agy', 'codex', 'gemini'].includes(account.provider); const { data: quota, isLoading: quotaLoading } = useAccountQuota( account.provider, account.id, - isAgy + isCliproxyProvider ); - // Show minimum quota of Claude models (primary), fallback to min of all models - const minQuota = quota?.success ? getMinClaudeQuota(quota.models) : null; + + // Get provider-specific minimum quota + const getProviderMinQuota = () => { + if (!quota?.success) return null; + switch (account.provider) { + case 'agy': + return getMinClaudeQuota((quota as QuotaResult).models); + case 'codex': + return getMinCodexQuota((quota as CodexQuotaResult).windows); + case 'gemini': + return getMinGeminiQuota((quota as GeminiCliQuotaResult).buckets); + default: + return null; + } + }; + const minQuota = getProviderMinQuota(); // Tier badge (AGY only) - show P for Pro, U for Ultra const showTierBadge = - isAgy && account.tier && account.tier !== 'unknown' && account.tier !== 'free'; + account.provider === 'agy' && + account.tier && + account.tier !== 'unknown' && + account.tier !== 'free'; return (
- {/* Quota bar for AGY accounts */} - {isAgy && ( + {/* Quota bar for CLIProxy accounts (agy, codex, gemini) */} + {isCliproxyProvider && (
{quotaLoading ? (
@@ -244,47 +266,99 @@ export function AccountCard({
-
-

Model Quotas:

- {(() => { - const tiered = getModelsWithTiers(quota?.models || []); - const groups = groupModelsByTier(tiered); - const tierOrder: ModelTier[] = ['primary', 'gemini-3', 'gemini-2', 'other']; - return tierOrder.map((tier, idx) => { - const models = groups.get(tier); - if (!models || models.length === 0) return null; - const isFirst = tierOrder - .slice(0, idx) - .every((t) => !groups.get(t)?.length); - return ( -
- {!isFirst &&
} - {models.map((m) => ( -
- - {m.displayName} - - - {m.percentage}% - -
- ))} + {account.provider === 'agy' ? ( +
+

Model Quotas:

+ {(() => { + const tiered = getModelsWithTiers((quota as QuotaResult)?.models || []); + const groups = groupModelsByTier(tiered); + const tierOrder: ModelTier[] = ['primary', 'gemini-3', 'gemini-2', 'other']; + return tierOrder.map((tier, idx) => { + const models = groups.get(tier); + if (!models || models.length === 0) return null; + const isFirst = tierOrder + .slice(0, idx) + .every((t) => !groups.get(t)?.length); + return ( +
+ {!isFirst &&
} + {models.map((m) => ( +
+ + {m.displayName} + + + {m.percentage}% + +
+ ))} +
+ ); + }); + })()} + {(() => { + const resetTime = getClaudeResetTime((quota as QuotaResult)?.models || []); + return resetTime ? ( +
+ + + Resets {formatResetTime(resetTime)} +
- ); - }); - })()} - {(() => { - const resetTime = getClaudeResetTime(quota?.models || []); - return resetTime ? ( -
- - - Resets {formatResetTime(resetTime)} + ) : null; + })()} +
+ ) : account.provider === 'codex' ? ( +
+

Rate Limits:

+ {(quota as CodexQuotaResult)?.windows?.map((w) => ( +
+ + {w.label} + {w.remainingPercent}%
- ) : null; - })()} -
+ ))} + {(() => { + const resetTime = getCodexResetTime( + (quota as CodexQuotaResult)?.windows || [] + ); + return resetTime ? ( +
+ + + Resets {formatResetTime(resetTime)} + +
+ ) : null; + })()} +
+ ) : account.provider === 'gemini' ? ( +
+

Buckets:

+ {(quota as GeminiCliQuotaResult)?.buckets?.map((b) => ( +
+ + {b.label} + + {b.remainingPercent}% +
+ ))} + {(() => { + const resetTime = getGeminiResetTime( + (quota as GeminiCliQuotaResult)?.buckets || [] + ); + return resetTime ? ( +
+ + + Resets {formatResetTime(resetTime)} + +
+ ) : null; + })()} +
+ ) : null} diff --git a/ui/src/components/cliproxy/provider-editor/account-item.tsx b/ui/src/components/cliproxy/provider-editor/account-item.tsx index fd6040e3..ef705c09 100644 --- a/ui/src/components/cliproxy/provider-editor/account-item.tsx +++ b/ui/src/components/cliproxy/provider-editor/account-item.tsx @@ -36,10 +36,20 @@ import { getMinClaudeQuota, getModelsWithTiers, groupModelsByTier, + getMinCodexQuota, + getMinGeminiQuota, + getCodexResetTime, + getGeminiResetTime, type ModelTier, } from '@/lib/utils'; import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context'; -import { useAccountQuota, useCliproxyStats } from '@/hooks/use-cliproxy-stats'; +import { + useAccountQuota, + useCliproxyStats, + type QuotaResult, + type CodexQuotaResult, + type GeminiCliQuotaResult, +} from '@/hooks/use-cliproxy-stats'; import type { AccountItemProps } from './types'; /** @@ -118,12 +128,152 @@ export function AccountItem({ const runtimeLastUsed = stats?.accountStats?.[account.email || account.id]?.lastUsedAt; const wasRecentlyUsed = isRecentlyUsed(runtimeLastUsed); - // Show minimum quota of Claude models (primary), fallback to min of all models - const minQuota = quota?.success ? getMinClaudeQuota(quota.models) : null; + // Compute min quota based on provider + const getProviderMinQuota = (): number | null => { + if (!quota?.success) return null; + switch (account.provider) { + case 'agy': + return getMinClaudeQuota((quota as QuotaResult).models); + case 'codex': + return getMinCodexQuota((quota as CodexQuotaResult).windows); + case 'gemini': + return getMinGeminiQuota((quota as GeminiCliQuotaResult).buckets); + default: + return null; + } + }; + const minQuota = getProviderMinQuota(); - // Get earliest reset time - const nextReset = - quota?.success && quota.models.length > 0 ? getClaudeResetTime(quota.models) : null; + // Compute reset time based on provider + const getProviderResetTime = (): string | null => { + if (!quota?.success) return null; + switch (account.provider) { + case 'agy': + return getClaudeResetTime((quota as QuotaResult).models); + case 'codex': + return getCodexResetTime((quota as CodexQuotaResult).windows); + case 'gemini': + return getGeminiResetTime((quota as GeminiCliQuotaResult).buckets); + default: + return null; + } + }; + const nextReset = getProviderResetTime(); + + // Render Antigravity (agy) provider tooltip + const renderAgyTooltip = () => { + const tiered = getModelsWithTiers((quota as QuotaResult).models || []); + const groups = groupModelsByTier(tiered); + const tierOrder: ModelTier[] = ['primary', 'gemini-3', 'gemini-2', 'other']; + return tierOrder.map((tier, idx) => { + const models = groups.get(tier); + if (!models || models.length === 0) return null; + const isFirst = tierOrder.slice(0, idx).every((t) => !groups.get(t)?.length); + return ( +
+ {!isFirst &&
} + {models.map((m) => ( +
+ {m.displayName} + + {m.percentage}% + +
+ ))} +
+ ); + }); + }; + + // Render Codex provider tooltip + const renderCodexTooltip = () => { + const windows = (quota as CodexQuotaResult).windows; + const planType = (quota as CodexQuotaResult).planType; + return ( + <> + {planType &&

Plan: {planType}

} + {windows.map((w) => ( +
+ {w.label} + {w.remainingPercent}% +
+ ))} + + ); + }; + + // Render Gemini provider tooltip + const renderGeminiTooltip = () => { + const buckets = (quota as GeminiCliQuotaResult).buckets; + return ( + <> + {buckets.map((b) => ( +
+ + {b.label} + {b.tokenType ? ` (${b.tokenType})` : ''} + + {b.remainingPercent}% +
+ ))} + + ); + }; + + // Provider-specific tooltip content renderer + const renderQuotaTooltip = () => { + if (!quota?.success) return null; + + switch (account.provider) { + case 'agy': + return ( +
+

Model Quotas:

+ {renderAgyTooltip()} + {nextReset && ( +
+ + + Resets {formatResetTime(nextReset)} + +
+ )} +
+ ); + case 'codex': + return ( +
+

Rate Limit Windows:

+ {renderCodexTooltip()} + {nextReset && ( +
+ + + Resets {formatResetTime(nextReset)} + +
+ )} +
+ ); + case 'gemini': + return ( +
+

Model Buckets:

+ {renderGeminiTooltip()} + {nextReset && ( +
+ + + Resets {formatResetTime(nextReset)} + +
+ )} +
+ ); + default: + return null; + } + }; return (
-
-

Model Quotas:

- {(() => { - const tiered = getModelsWithTiers(quota?.models || []); - const groups = groupModelsByTier(tiered); - const tierOrder: ModelTier[] = ['primary', 'gemini-3', 'gemini-2', 'other']; - return tierOrder.map((tier, idx) => { - const models = groups.get(tier); - if (!models || models.length === 0) return null; - const isFirst = tierOrder - .slice(0, idx) - .every((t) => !groups.get(t)?.length); - return ( -
- {!isFirst &&
} - {models.map((m) => ( -
- - {m.displayName} - - - {m.percentage}% - -
- ))} -
- ); - }); - })()} - {nextReset && ( -
- - - Resets {formatResetTime(nextReset)} - -
- )} -
+ {renderQuotaTooltip()} diff --git a/ui/src/components/cliproxy/provider-editor/model-config-tab.tsx b/ui/src/components/cliproxy/provider-editor/model-config-tab.tsx index 62eeadd8..464e30b4 100644 --- a/ui/src/components/cliproxy/provider-editor/model-config-tab.tsx +++ b/ui/src/components/cliproxy/provider-editor/model-config-tab.tsx @@ -167,7 +167,7 @@ export function ModelConfigTab({ isBulkPausing={isBulkPausing} isBulkResuming={isBulkResuming} privacyMode={privacyMode} - showQuota={provider === 'agy' && !isRemoteMode} + showQuota={['agy', 'codex', 'gemini'].includes(provider) && !isRemoteMode} isKiro={isKiro} kiroNoIncognito={kiroNoIncognito} onKiroNoIncognitoChange={saveKiroNoIncognito}