mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-12 20:15:40 +00:00
refactor(ui): extract duplicated quota helpers to shared utils
Address PR review feedback: - Add type guards (isAgyQuotaResult, isCodexQuotaResult, isGeminiQuotaResult) - Add unified getProviderMinQuota() and getProviderResetTime() helpers - Export QUOTA_SUPPORTED_PROVIDERS constant from hooks - Update account-item.tsx and account-card.tsx to use shared functions - Eliminate code duplication across provider-specific quota handling Closes #400 review feedback
This commit is contained in:
1 parent
387c010267
commit
8ce749581e
4 files changed
+208
-192
No files matched your search
@@ -5,20 +5,19 @@
|
|||||||
import {
|
import {
|
||||||
cn,
|
cn,
|
||||||
formatResetTime,
|
formatResetTime,
|
||||||
getClaudeResetTime,
|
|
||||||
getMinClaudeQuota,
|
|
||||||
getModelsWithTiers,
|
getModelsWithTiers,
|
||||||
groupModelsByTier,
|
groupModelsByTier,
|
||||||
getMinCodexQuota,
|
getProviderMinQuota,
|
||||||
getMinGeminiQuota,
|
getProviderResetTime,
|
||||||
getCodexResetTime,
|
isAgyQuotaResult,
|
||||||
getGeminiResetTime,
|
isCodexQuotaResult,
|
||||||
|
isGeminiQuotaResult,
|
||||||
type ModelTier,
|
type ModelTier,
|
||||||
} from '@/lib/utils';
|
} from '@/lib/utils';
|
||||||
import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
|
import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
|
||||||
import { GripVertical, Loader2, Clock, Pause, Play } from 'lucide-react';
|
import { GripVertical, Loader2, Clock, Pause, Play } from 'lucide-react';
|
||||||
import { useAccountQuota } from '@/hooks/use-cliproxy-stats';
|
import { useAccountQuota, QUOTA_SUPPORTED_PROVIDERS } from '@/hooks/use-cliproxy-stats';
|
||||||
import type { CodexQuotaResult, GeminiCliQuotaResult, QuotaResult } from '@/lib/api-client';
|
import type { QuotaSupportedProvider } from '@/hooks/use-cliproxy-stats';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
@@ -95,28 +94,17 @@ export function AccountCard({
|
|||||||
const connectorPosition = CONNECTOR_POSITION_MAP[zone];
|
const connectorPosition = CONNECTOR_POSITION_MAP[zone];
|
||||||
|
|
||||||
// Quota for CLIProxy accounts (agy, codex, gemini)
|
// Quota for CLIProxy accounts (agy, codex, gemini)
|
||||||
const isCliproxyProvider = ['agy', 'codex', 'gemini'].includes(account.provider);
|
const isCliproxyProvider = QUOTA_SUPPORTED_PROVIDERS.includes(
|
||||||
|
account.provider as QuotaSupportedProvider
|
||||||
|
);
|
||||||
const { data: quota, isLoading: quotaLoading } = useAccountQuota(
|
const { data: quota, isLoading: quotaLoading } = useAccountQuota(
|
||||||
account.provider,
|
account.provider,
|
||||||
account.id,
|
account.id,
|
||||||
isCliproxyProvider
|
isCliproxyProvider
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get provider-specific minimum quota
|
// Use shared helper for provider-specific minimum quota
|
||||||
const getProviderMinQuota = () => {
|
const minQuota = getProviderMinQuota(account.provider, quota);
|
||||||
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
|
// Tier badge (AGY only) - show P for Pro, U for Ultra
|
||||||
const showTierBadge =
|
const showTierBadge =
|
||||||
@@ -266,11 +254,11 @@ export function AccountCard({
|
|||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="top" className="max-w-xs">
|
<TooltipContent side="top" className="max-w-xs">
|
||||||
{account.provider === 'agy' ? (
|
{quota && isAgyQuotaResult(quota) ? (
|
||||||
<div className="text-xs space-y-1">
|
<div className="text-xs space-y-1">
|
||||||
<p className="font-medium">Model Quotas:</p>
|
<p className="font-medium">Model Quotas:</p>
|
||||||
{(() => {
|
{(() => {
|
||||||
const tiered = getModelsWithTiers((quota as QuotaResult)?.models || []);
|
const tiered = getModelsWithTiers(quota.models || []);
|
||||||
const groups = groupModelsByTier(tiered);
|
const groups = groupModelsByTier(tiered);
|
||||||
const tierOrder: ModelTier[] = ['primary', 'gemini-3', 'gemini-2', 'other'];
|
const tierOrder: ModelTier[] = ['primary', 'gemini-3', 'gemini-2', 'other'];
|
||||||
return tierOrder.map((tier, idx) => {
|
return tierOrder.map((tier, idx) => {
|
||||||
@@ -297,7 +285,7 @@ export function AccountCard({
|
|||||||
});
|
});
|
||||||
})()}
|
})()}
|
||||||
{(() => {
|
{(() => {
|
||||||
const resetTime = getClaudeResetTime((quota as QuotaResult)?.models || []);
|
const resetTime = getProviderResetTime('agy', quota);
|
||||||
return resetTime ? (
|
return resetTime ? (
|
||||||
<div className="flex items-center gap-1.5 pt-1 border-t border-border/50">
|
<div className="flex items-center gap-1.5 pt-1 border-t border-border/50">
|
||||||
<Clock className="w-3 h-3 text-blue-400" />
|
<Clock className="w-3 h-3 text-blue-400" />
|
||||||
@@ -308,10 +296,10 @@ export function AccountCard({
|
|||||||
) : null;
|
) : null;
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
) : account.provider === 'codex' ? (
|
) : quota && isCodexQuotaResult(quota) ? (
|
||||||
<div className="text-xs space-y-1">
|
<div className="text-xs space-y-1">
|
||||||
<p className="font-medium">Rate Limits:</p>
|
<p className="font-medium">Rate Limits:</p>
|
||||||
{(quota as CodexQuotaResult)?.windows?.map((w) => (
|
{quota.windows?.map((w) => (
|
||||||
<div key={w.label} className="flex justify-between gap-4">
|
<div key={w.label} className="flex justify-between gap-4">
|
||||||
<span className={cn(w.remainingPercent < 20 && 'text-red-500')}>
|
<span className={cn(w.remainingPercent < 20 && 'text-red-500')}>
|
||||||
{w.label}
|
{w.label}
|
||||||
@@ -320,9 +308,7 @@ export function AccountCard({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{(() => {
|
{(() => {
|
||||||
const resetTime = getCodexResetTime(
|
const resetTime = getProviderResetTime('codex', quota);
|
||||||
(quota as CodexQuotaResult)?.windows || []
|
|
||||||
);
|
|
||||||
return resetTime ? (
|
return resetTime ? (
|
||||||
<div className="flex items-center gap-1.5 pt-1 border-t border-border/50">
|
<div className="flex items-center gap-1.5 pt-1 border-t border-border/50">
|
||||||
<Clock className="w-3 h-3 text-blue-400" />
|
<Clock className="w-3 h-3 text-blue-400" />
|
||||||
@@ -333,10 +319,10 @@ export function AccountCard({
|
|||||||
) : null;
|
) : null;
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
) : account.provider === 'gemini' ? (
|
) : quota && isGeminiQuotaResult(quota) ? (
|
||||||
<div className="text-xs space-y-1">
|
<div className="text-xs space-y-1">
|
||||||
<p className="font-medium">Buckets:</p>
|
<p className="font-medium">Buckets:</p>
|
||||||
{(quota as GeminiCliQuotaResult)?.buckets?.map((b) => (
|
{quota.buckets?.map((b) => (
|
||||||
<div key={b.id} className="flex justify-between gap-4">
|
<div key={b.id} className="flex justify-between gap-4">
|
||||||
<span className={cn(b.remainingPercent < 20 && 'text-red-500')}>
|
<span className={cn(b.remainingPercent < 20 && 'text-red-500')}>
|
||||||
{b.label}
|
{b.label}
|
||||||
@@ -345,9 +331,7 @@ export function AccountCard({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{(() => {
|
{(() => {
|
||||||
const resetTime = getGeminiResetTime(
|
const resetTime = getProviderResetTime('gemini', quota);
|
||||||
(quota as GeminiCliQuotaResult)?.buckets || []
|
|
||||||
);
|
|
||||||
return resetTime ? (
|
return resetTime ? (
|
||||||
<div className="flex items-center gap-1.5 pt-1 border-t border-border/50">
|
<div className="flex items-center gap-1.5 pt-1 border-t border-border/50">
|
||||||
<Clock className="w-3 h-3 text-blue-400" />
|
<Clock className="w-3 h-3 text-blue-400" />
|
||||||
|
|||||||
@@ -32,24 +32,17 @@ import {
|
|||||||
import {
|
import {
|
||||||
cn,
|
cn,
|
||||||
formatResetTime,
|
formatResetTime,
|
||||||
getClaudeResetTime,
|
|
||||||
getMinClaudeQuota,
|
|
||||||
getModelsWithTiers,
|
getModelsWithTiers,
|
||||||
groupModelsByTier,
|
groupModelsByTier,
|
||||||
getMinCodexQuota,
|
getProviderMinQuota,
|
||||||
getMinGeminiQuota,
|
getProviderResetTime,
|
||||||
getCodexResetTime,
|
isAgyQuotaResult,
|
||||||
getGeminiResetTime,
|
isCodexQuotaResult,
|
||||||
|
isGeminiQuotaResult,
|
||||||
type ModelTier,
|
type ModelTier,
|
||||||
} from '@/lib/utils';
|
} from '@/lib/utils';
|
||||||
import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
|
import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
|
||||||
import {
|
import { useAccountQuota, useCliproxyStats } from '@/hooks/use-cliproxy-stats';
|
||||||
useAccountQuota,
|
|
||||||
useCliproxyStats,
|
|
||||||
type QuotaResult,
|
|
||||||
type CodexQuotaResult,
|
|
||||||
type GeminiCliQuotaResult,
|
|
||||||
} from '@/hooks/use-cliproxy-stats';
|
|
||||||
import type { AccountItemProps } from './types';
|
import type { AccountItemProps } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -128,151 +121,99 @@ export function AccountItem({
|
|||||||
const runtimeLastUsed = stats?.accountStats?.[account.email || account.id]?.lastUsedAt;
|
const runtimeLastUsed = stats?.accountStats?.[account.email || account.id]?.lastUsedAt;
|
||||||
const wasRecentlyUsed = isRecentlyUsed(runtimeLastUsed);
|
const wasRecentlyUsed = isRecentlyUsed(runtimeLastUsed);
|
||||||
|
|
||||||
// Compute min quota based on provider
|
// Use shared utility functions for provider-specific quota handling
|
||||||
const getProviderMinQuota = (): number | null => {
|
const minQuota = getProviderMinQuota(account.provider, quota);
|
||||||
if (!quota?.success) return null;
|
const nextReset = getProviderResetTime(account.provider, quota);
|
||||||
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();
|
|
||||||
|
|
||||||
// Compute reset time based on provider
|
// Provider-specific tooltip content renderer using type guards
|
||||||
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 (
|
|
||||||
<div key={tier}>
|
|
||||||
{!isFirst && <div className="border-t border-border/40 my-1" />}
|
|
||||||
{models.map((m) => (
|
|
||||||
<div key={m.name} className="flex justify-between gap-4">
|
|
||||||
<span className={cn('truncate', m.exhausted && 'text-red-500')}>{m.displayName}</span>
|
|
||||||
<span className={cn('font-mono', m.exhausted && 'text-red-500')}>
|
|
||||||
{m.percentage}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Render Codex provider tooltip
|
|
||||||
const renderCodexTooltip = () => {
|
|
||||||
const windows = (quota as CodexQuotaResult).windows;
|
|
||||||
const planType = (quota as CodexQuotaResult).planType;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{planType && <p className="text-muted-foreground">Plan: {planType}</p>}
|
|
||||||
{windows.map((w) => (
|
|
||||||
<div key={w.label} className="flex justify-between gap-4">
|
|
||||||
<span>{w.label}</span>
|
|
||||||
<span className="font-mono">{w.remainingPercent}%</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Render Gemini provider tooltip
|
|
||||||
const renderGeminiTooltip = () => {
|
|
||||||
const buckets = (quota as GeminiCliQuotaResult).buckets;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{buckets.map((b) => (
|
|
||||||
<div key={b.id} className="flex justify-between gap-4">
|
|
||||||
<span>
|
|
||||||
{b.label}
|
|
||||||
{b.tokenType ? ` (${b.tokenType})` : ''}
|
|
||||||
</span>
|
|
||||||
<span className="font-mono">{b.remainingPercent}%</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Provider-specific tooltip content renderer
|
|
||||||
const renderQuotaTooltip = () => {
|
const renderQuotaTooltip = () => {
|
||||||
if (!quota?.success) return null;
|
if (!quota?.success) return null;
|
||||||
|
|
||||||
switch (account.provider) {
|
// Antigravity (agy) provider tooltip
|
||||||
case 'agy':
|
if (isAgyQuotaResult(quota)) {
|
||||||
return (
|
const tiered = getModelsWithTiers(quota.models || []);
|
||||||
<div className="text-xs space-y-1">
|
const groups = groupModelsByTier(tiered);
|
||||||
<p className="font-medium">Model Quotas:</p>
|
const tierOrder: ModelTier[] = ['primary', 'gemini-3', 'gemini-2', 'other'];
|
||||||
{renderAgyTooltip()}
|
return (
|
||||||
{nextReset && (
|
<div className="text-xs space-y-1">
|
||||||
<div className="flex items-center gap-1.5 pt-1 border-t border-border/50">
|
<p className="font-medium">Model Quotas:</p>
|
||||||
<Clock className="w-3 h-3 text-blue-400" />
|
{tierOrder.map((tier, idx) => {
|
||||||
<span className="text-blue-400 font-medium">
|
const models = groups.get(tier);
|
||||||
Resets {formatResetTime(nextReset)}
|
if (!models || models.length === 0) return null;
|
||||||
</span>
|
const isFirst = tierOrder.slice(0, idx).every((t) => !groups.get(t)?.length);
|
||||||
|
return (
|
||||||
|
<div key={tier}>
|
||||||
|
{!isFirst && <div className="border-t border-border/40 my-1" />}
|
||||||
|
{models.map((m) => (
|
||||||
|
<div key={m.name} className="flex justify-between gap-4">
|
||||||
|
<span className={cn('truncate', m.exhausted && 'text-red-500')}>
|
||||||
|
{m.displayName}
|
||||||
|
</span>
|
||||||
|
<span className={cn('font-mono', m.exhausted && 'text-red-500')}>
|
||||||
|
{m.percentage}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
</div>
|
})}
|
||||||
);
|
{nextReset && (
|
||||||
case 'codex':
|
<div className="flex items-center gap-1.5 pt-1 border-t border-border/50">
|
||||||
return (
|
<Clock className="w-3 h-3 text-blue-400" />
|
||||||
<div className="text-xs space-y-1">
|
<span className="text-blue-400 font-medium">Resets {formatResetTime(nextReset)}</span>
|
||||||
<p className="font-medium">Rate Limit Windows:</p>
|
</div>
|
||||||
{renderCodexTooltip()}
|
)}
|
||||||
{nextReset && (
|
</div>
|
||||||
<div className="flex items-center gap-1.5 pt-1 border-t border-border/50">
|
);
|
||||||
<Clock className="w-3 h-3 text-blue-400" />
|
|
||||||
<span className="text-blue-400 font-medium">
|
|
||||||
Resets {formatResetTime(nextReset)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case 'gemini':
|
|
||||||
return (
|
|
||||||
<div className="text-xs space-y-1">
|
|
||||||
<p className="font-medium">Model Buckets:</p>
|
|
||||||
{renderGeminiTooltip()}
|
|
||||||
{nextReset && (
|
|
||||||
<div className="flex items-center gap-1.5 pt-1 border-t border-border/50">
|
|
||||||
<Clock className="w-3 h-3 text-blue-400" />
|
|
||||||
<span className="text-blue-400 font-medium">
|
|
||||||
Resets {formatResetTime(nextReset)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Codex provider tooltip
|
||||||
|
if (isCodexQuotaResult(quota)) {
|
||||||
|
return (
|
||||||
|
<div className="text-xs space-y-1">
|
||||||
|
<p className="font-medium">Rate Limit Windows:</p>
|
||||||
|
{quota.planType && <p className="text-muted-foreground">Plan: {quota.planType}</p>}
|
||||||
|
{quota.windows.map((w) => (
|
||||||
|
<div key={w.label} className="flex justify-between gap-4">
|
||||||
|
<span>{w.label}</span>
|
||||||
|
<span className="font-mono">{w.remainingPercent}%</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{nextReset && (
|
||||||
|
<div className="flex items-center gap-1.5 pt-1 border-t border-border/50">
|
||||||
|
<Clock className="w-3 h-3 text-blue-400" />
|
||||||
|
<span className="text-blue-400 font-medium">Resets {formatResetTime(nextReset)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gemini provider tooltip
|
||||||
|
if (isGeminiQuotaResult(quota)) {
|
||||||
|
return (
|
||||||
|
<div className="text-xs space-y-1">
|
||||||
|
<p className="font-medium">Model Buckets:</p>
|
||||||
|
{quota.buckets.map((b) => (
|
||||||
|
<div key={b.id} className="flex justify-between gap-4">
|
||||||
|
<span>
|
||||||
|
{b.label}
|
||||||
|
{b.tokenType ? ` (${b.tokenType})` : ''}
|
||||||
|
</span>
|
||||||
|
<span className="font-mono">{b.remainingPercent}%</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{nextReset && (
|
||||||
|
<div className="flex items-center gap-1.5 pt-1 border-t border-border/50">
|
||||||
|
<Clock className="w-3 h-3 text-blue-400" />
|
||||||
|
<span className="text-blue-400 font-medium">Resets {formatResetTime(nextReset)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -204,7 +204,8 @@ export function useCliproxyErrorLogContent(name: string | null) {
|
|||||||
export type { ModelQuota, QuotaResult, CodexQuotaResult, GeminiCliQuotaResult };
|
export type { ModelQuota, QuotaResult, CodexQuotaResult, GeminiCliQuotaResult };
|
||||||
|
|
||||||
/** Providers with quota API support */
|
/** Providers with quota API support */
|
||||||
const SUPPORTED_PROVIDERS = ['agy', 'codex', 'gemini'] as const;
|
export const QUOTA_SUPPORTED_PROVIDERS = ['agy', 'codex', 'gemini'] as const;
|
||||||
|
export type QuotaSupportedProvider = (typeof QUOTA_SUPPORTED_PROVIDERS)[number];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch account quota from API (Antigravity only)
|
* Fetch account quota from API (Antigravity only)
|
||||||
@@ -290,7 +291,7 @@ export function useAccountQuota(provider: string, accountId: string, enabled = t
|
|||||||
queryFn: () => fetchQuotaByProvider(provider, accountId),
|
queryFn: () => fetchQuotaByProvider(provider, accountId),
|
||||||
enabled:
|
enabled:
|
||||||
enabled &&
|
enabled &&
|
||||||
SUPPORTED_PROVIDERS.includes(provider as (typeof SUPPORTED_PROVIDERS)[number]) &&
|
QUOTA_SUPPORTED_PROVIDERS.includes(provider as QuotaSupportedProvider) &&
|
||||||
!!accountId,
|
!!accountId,
|
||||||
staleTime: 60000, // Match refetchInterval to prevent early refetching
|
staleTime: 60000, // Match refetchInterval to prevent early refetching
|
||||||
refetchInterval: 60000, // Refresh every 1 minute
|
refetchInterval: 60000, // Refresh every 1 minute
|
||||||
|
|||||||
+91
-1
@@ -1,6 +1,12 @@
|
|||||||
import { clsx, type ClassValue } from 'clsx';
|
import { clsx, type ClassValue } from 'clsx';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
import type { CodexQuotaWindow, GeminiCliBucket } from './api-client';
|
import type {
|
||||||
|
CodexQuotaWindow,
|
||||||
|
CodexQuotaResult,
|
||||||
|
GeminiCliBucket,
|
||||||
|
GeminiCliQuotaResult,
|
||||||
|
QuotaResult,
|
||||||
|
} from './api-client';
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
@@ -337,3 +343,87 @@ export function getGeminiResetTime(buckets: GeminiCliBucket[]): string | null {
|
|||||||
if (resets.length === 0) return null;
|
if (resets.length === 0) return null;
|
||||||
return resets.sort()[0];
|
return resets.sort()[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== Unified Quota Type Guards ====================
|
||||||
|
|
||||||
|
/** Unified quota result type for provider-agnostic handling */
|
||||||
|
export type UnifiedQuotaResult = QuotaResult | CodexQuotaResult | GeminiCliQuotaResult;
|
||||||
|
|
||||||
|
/** Type guard: Check if quota result is from Antigravity (agy) provider */
|
||||||
|
export function isAgyQuotaResult(quota: UnifiedQuotaResult): quota is QuotaResult {
|
||||||
|
return 'models' in quota && Array.isArray((quota as QuotaResult).models);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Type guard: Check if quota result is from Codex provider */
|
||||||
|
export function isCodexQuotaResult(quota: UnifiedQuotaResult): quota is CodexQuotaResult {
|
||||||
|
return 'windows' in quota && Array.isArray((quota as CodexQuotaResult).windows);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Type guard: Check if quota result is from Gemini CLI provider */
|
||||||
|
export function isGeminiQuotaResult(quota: UnifiedQuotaResult): quota is GeminiCliQuotaResult {
|
||||||
|
return 'buckets' in quota && Array.isArray((quota as GeminiCliQuotaResult).buckets);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Unified Quota Helpers ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get minimum quota percentage for any provider
|
||||||
|
* Centralizes provider-specific logic to eliminate duplication
|
||||||
|
*/
|
||||||
|
export function getProviderMinQuota(
|
||||||
|
provider: string,
|
||||||
|
quota: UnifiedQuotaResult | null | undefined
|
||||||
|
): number | null {
|
||||||
|
if (!quota?.success) return null;
|
||||||
|
|
||||||
|
switch (provider) {
|
||||||
|
case 'agy':
|
||||||
|
if (isAgyQuotaResult(quota)) {
|
||||||
|
return getMinClaudeQuota(quota.models);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
case 'codex':
|
||||||
|
if (isCodexQuotaResult(quota)) {
|
||||||
|
return getMinCodexQuota(quota.windows);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
case 'gemini':
|
||||||
|
if (isGeminiQuotaResult(quota)) {
|
||||||
|
return getMinGeminiQuota(quota.buckets);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get earliest reset time for any provider
|
||||||
|
* Centralizes provider-specific logic to eliminate duplication
|
||||||
|
*/
|
||||||
|
export function getProviderResetTime(
|
||||||
|
provider: string,
|
||||||
|
quota: UnifiedQuotaResult | null | undefined
|
||||||
|
): string | null {
|
||||||
|
if (!quota?.success) return null;
|
||||||
|
|
||||||
|
switch (provider) {
|
||||||
|
case 'agy':
|
||||||
|
if (isAgyQuotaResult(quota)) {
|
||||||
|
return getClaudeResetTime(quota.models);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
case 'codex':
|
||||||
|
if (isCodexQuotaResult(quota)) {
|
||||||
|
return getCodexResetTime(quota.windows);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
case 'gemini':
|
||||||
|
if (isGeminiQuotaResult(quota)) {
|
||||||
|
return getGeminiResetTime(quota.buckets);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user