mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-15 22:14:40 +00:00
fix(cliproxy): close remaining quota edge-case gaps
This commit is contained in:
1 parent
9371a72c16
commit
8c790f41ff
10 files changed
+482
-80
No files matched your search
@@ -24,6 +24,13 @@ import { cleanEmail } from './utils';
|
||||
import { AccountCardStats } from './account-card-stats';
|
||||
|
||||
type Zone = 'left' | 'right' | 'top' | 'bottom';
|
||||
const QUOTA_PROVIDER_ALIASES = [
|
||||
'antigravity',
|
||||
'anthropic',
|
||||
'gemini-cli',
|
||||
'copilot',
|
||||
'github-copilot',
|
||||
];
|
||||
|
||||
interface AccountCardProps {
|
||||
account: AccountData;
|
||||
@@ -92,11 +99,14 @@ export function AccountCard({
|
||||
const connectorPosition = CONNECTOR_POSITION_MAP[zone];
|
||||
|
||||
// Quota for CLIProxy accounts (agy, codex, claude, gemini, ghcp)
|
||||
const isCliproxyProvider = QUOTA_SUPPORTED_PROVIDERS.includes(
|
||||
account.provider as QuotaSupportedProvider
|
||||
);
|
||||
const normalizedProvider = account.provider.toLowerCase();
|
||||
const isCliproxyProvider =
|
||||
QUOTA_SUPPORTED_PROVIDERS.includes(normalizedProvider as QuotaSupportedProvider) ||
|
||||
QUOTA_PROVIDER_ALIASES.includes(normalizedProvider);
|
||||
const isCodexProvider = normalizedProvider === 'codex';
|
||||
const isClaudeProvider = normalizedProvider === 'claude' || normalizedProvider === 'anthropic';
|
||||
const { data: quota, isLoading: quotaLoading } = useAccountQuota(
|
||||
account.provider,
|
||||
normalizedProvider,
|
||||
account.id,
|
||||
isCliproxyProvider
|
||||
);
|
||||
@@ -105,7 +115,7 @@ export function AccountCard({
|
||||
const minQuota = getProviderMinQuota(account.provider, quota);
|
||||
const resetTime = getProviderResetTime(account.provider, quota);
|
||||
const codexBreakdown =
|
||||
account.provider === 'codex' && quota && isCodexQuotaResult(quota)
|
||||
isCodexProvider && quota && isCodexQuotaResult(quota)
|
||||
? getCodexQuotaBreakdown(quota.windows)
|
||||
: null;
|
||||
const codexQuotaRows = [
|
||||
@@ -113,7 +123,7 @@ export function AccountCard({
|
||||
{ label: 'Wk', value: codexBreakdown?.weeklyWindow?.remainingPercent ?? null },
|
||||
].filter((row): row is { label: string; value: number } => row.value !== null);
|
||||
const claudeQuotaRows =
|
||||
account.provider === 'claude' && quota && isClaudeQuotaResult(quota)
|
||||
isClaudeProvider && quota && isClaudeQuotaResult(quota)
|
||||
? [
|
||||
{
|
||||
label: '5h',
|
||||
@@ -140,13 +150,13 @@ export function AccountCard({
|
||||
},
|
||||
].filter((row): row is { label: string; value: number } => row.value !== null)
|
||||
: [];
|
||||
const compactQuotaRows =
|
||||
account.provider === 'codex'
|
||||
? codexQuotaRows
|
||||
: account.provider === 'claude'
|
||||
? claudeQuotaRows
|
||||
: [];
|
||||
const compactQuotaRows = isCodexProvider
|
||||
? codexQuotaRows
|
||||
: isClaudeProvider
|
||||
? claudeQuotaRows
|
||||
: [];
|
||||
const minQuotaLabel = minQuota !== null ? formatQuotaPercent(minQuota) : null;
|
||||
const minQuotaValue = minQuotaLabel !== null ? Number(minQuotaLabel) : null;
|
||||
|
||||
// Tier badge (AGY only) - show P for Pro, U for Ultra
|
||||
const showTierBadge =
|
||||
@@ -258,7 +268,7 @@ export function AccountCard({
|
||||
<Loader2 className="w-2.5 h-2.5 animate-spin" />
|
||||
<span>Quota...</span>
|
||||
</div>
|
||||
) : minQuota !== null ? (
|
||||
) : minQuotaValue !== null ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -270,9 +280,9 @@ export function AccountCard({
|
||||
<span
|
||||
className={cn(
|
||||
'text-[10px] font-mono font-bold',
|
||||
minQuota > 50
|
||||
minQuotaValue > 50
|
||||
? 'text-emerald-600 dark:text-emerald-400'
|
||||
: minQuota > 20
|
||||
: minQuotaValue > 20
|
||||
? 'text-amber-500'
|
||||
: 'text-red-500'
|
||||
)}
|
||||
@@ -293,13 +303,13 @@ export function AccountCard({
|
||||
<div
|
||||
className={cn(
|
||||
'h-full rounded-full transition-all',
|
||||
minQuota > 50
|
||||
minQuotaValue > 50
|
||||
? 'bg-emerald-500'
|
||||
: minQuota > 20
|
||||
: minQuotaValue > 20
|
||||
? 'bg-amber-500'
|
||||
: 'bg-red-500'
|
||||
)}
|
||||
style={{ width: `${minQuota}%` }}
|
||||
style={{ width: `${minQuotaValue}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -309,6 +319,8 @@ export function AccountCard({
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : quota?.success ? (
|
||||
<div className="text-[8px] text-muted-foreground/60">Quota limits unavailable</div>
|
||||
) : quota?.needsReauth ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
|
||||
@@ -106,12 +106,16 @@ export function AccountItem({
|
||||
selected,
|
||||
onSelectChange,
|
||||
}: AccountItemProps) {
|
||||
const normalizedProvider = account.provider.toLowerCase();
|
||||
const isCodexProvider = normalizedProvider === 'codex';
|
||||
const isClaudeProvider = normalizedProvider === 'claude' || normalizedProvider === 'anthropic';
|
||||
|
||||
// Fetch runtime stats to get actual lastUsedAt (more accurate than file state)
|
||||
const { data: stats } = useCliproxyStats(showQuota);
|
||||
|
||||
// Fetch quota for all provider accounts
|
||||
const { data: quota, isLoading: quotaLoading } = useAccountQuota(
|
||||
account.provider,
|
||||
normalizedProvider,
|
||||
account.id,
|
||||
showQuota
|
||||
);
|
||||
@@ -124,7 +128,7 @@ export function AccountItem({
|
||||
const minQuota = getProviderMinQuota(account.provider, quota);
|
||||
const nextReset = getProviderResetTime(account.provider, quota);
|
||||
const codexBreakdown =
|
||||
account.provider === 'codex' && quota && isCodexQuotaResult(quota)
|
||||
isCodexProvider && quota && isCodexQuotaResult(quota)
|
||||
? getCodexQuotaBreakdown(quota.windows)
|
||||
: null;
|
||||
const codexQuotaRows = [
|
||||
@@ -132,7 +136,7 @@ export function AccountItem({
|
||||
{ label: 'Weekly', value: codexBreakdown?.weeklyWindow?.remainingPercent ?? null },
|
||||
].filter((row): row is { label: string; value: number } => row.value !== null);
|
||||
const claudeQuotaRows =
|
||||
account.provider === 'claude' && quota && isClaudeQuotaResult(quota)
|
||||
isClaudeProvider && quota && isClaudeQuotaResult(quota)
|
||||
? [
|
||||
{
|
||||
label: '5h',
|
||||
@@ -159,13 +163,13 @@ export function AccountItem({
|
||||
},
|
||||
].filter((row): row is { label: string; value: number } => row.value !== null)
|
||||
: [];
|
||||
const dualWindowQuotaRows =
|
||||
account.provider === 'codex'
|
||||
? codexQuotaRows
|
||||
: account.provider === 'claude'
|
||||
? claudeQuotaRows
|
||||
: [];
|
||||
const dualWindowQuotaRows = isCodexProvider
|
||||
? codexQuotaRows
|
||||
: isClaudeProvider
|
||||
? claudeQuotaRows
|
||||
: [];
|
||||
const minQuotaLabel = minQuota !== null ? formatQuotaPercent(minQuota) : null;
|
||||
const minQuotaValue = minQuotaLabel !== null ? Number(minQuotaLabel) : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -359,7 +363,7 @@ export function AccountItem({
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
<span>Loading quota...</span>
|
||||
</div>
|
||||
) : minQuota !== null ? (
|
||||
) : minQuotaValue !== null ? (
|
||||
<div className="space-y-1.5">
|
||||
{/* Status indicator based on runtime usage, not file state */}
|
||||
<div className="flex items-center gap-1.5 text-xs">
|
||||
@@ -409,9 +413,9 @@ export function AccountItem({
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<Progress
|
||||
value={Math.max(0, Math.min(100, minQuota))}
|
||||
value={Math.max(0, Math.min(100, minQuotaValue))}
|
||||
className="h-2 flex-1"
|
||||
indicatorClassName={getQuotaColor(minQuota)}
|
||||
indicatorClassName={getQuotaColor(minQuotaValue)}
|
||||
/>
|
||||
<span className="text-xs font-medium w-10 text-right">
|
||||
{minQuotaLabel}%
|
||||
@@ -425,6 +429,16 @@ export function AccountItem({
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
) : quota?.success ? (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[10px] h-5 px-2 gap-1 border-muted-foreground/50 text-muted-foreground"
|
||||
>
|
||||
<HelpCircle className="w-3 h-3" />
|
||||
No limits
|
||||
</Badge>
|
||||
</div>
|
||||
) : quota?.needsReauth ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
|
||||
@@ -94,6 +94,14 @@ export function ModelConfigTab({
|
||||
privacyMode,
|
||||
isRemoteMode,
|
||||
}: ModelConfigTabProps) {
|
||||
const normalizedProvider = provider.toLowerCase();
|
||||
const showQuota =
|
||||
(QUOTA_SUPPORTED_PROVIDERS.includes(normalizedProvider as QuotaSupportedProvider) ||
|
||||
['anthropic', 'antigravity', 'gemini-cli', 'copilot', 'github-copilot'].includes(
|
||||
normalizedProvider
|
||||
)) &&
|
||||
!isRemoteMode;
|
||||
|
||||
// Kiro-specific: no-incognito setting (defaults to true = normal browser)
|
||||
const isKiro = provider === 'kiro';
|
||||
const [kiroNoIncognito, setKiroNoIncognito] = useState(true);
|
||||
@@ -177,9 +185,7 @@ export function ModelConfigTab({
|
||||
isBulkPausing={isBulkPausing}
|
||||
isBulkResuming={isBulkResuming}
|
||||
privacyMode={privacyMode}
|
||||
showQuota={
|
||||
QUOTA_SUPPORTED_PROVIDERS.includes(provider as QuotaSupportedProvider) && !isRemoteMode
|
||||
}
|
||||
showQuota={showQuota}
|
||||
isKiro={isKiro}
|
||||
kiroNoIncognito={kiroNoIncognito}
|
||||
onKiroNoIncognitoChange={saveKiroNoIncognito}
|
||||
|
||||
@@ -216,6 +216,26 @@ export type {
|
||||
/** Providers with quota API support */
|
||||
export const QUOTA_SUPPORTED_PROVIDERS = ['agy', 'codex', 'claude', 'gemini', 'ghcp'] as const;
|
||||
export type QuotaSupportedProvider = (typeof QUOTA_SUPPORTED_PROVIDERS)[number];
|
||||
const QUOTA_PROVIDER_ALIAS_MAP: Readonly<Record<string, QuotaSupportedProvider>> = {
|
||||
antigravity: 'agy',
|
||||
anthropic: 'claude',
|
||||
'gemini-cli': 'gemini',
|
||||
copilot: 'ghcp',
|
||||
'github-copilot': 'ghcp',
|
||||
};
|
||||
|
||||
function normalizeQuotaProvider(provider: string): QuotaSupportedProvider | null {
|
||||
const normalized = provider.trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((QUOTA_SUPPORTED_PROVIDERS as readonly string[]).includes(normalized)) {
|
||||
return normalized as QuotaSupportedProvider;
|
||||
}
|
||||
|
||||
return QUOTA_PROVIDER_ALIAS_MAP[normalized] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch account quota from generic API route
|
||||
@@ -317,7 +337,12 @@ async function fetchQuotaByProvider(
|
||||
provider: string,
|
||||
accountId: string
|
||||
): Promise<UnifiedQuotaResult> {
|
||||
switch (provider) {
|
||||
const canonicalProvider = normalizeQuotaProvider(provider);
|
||||
if (!canonicalProvider) {
|
||||
return fetchAccountQuota(provider, accountId);
|
||||
}
|
||||
|
||||
switch (canonicalProvider) {
|
||||
case 'codex':
|
||||
return fetchCodexQuotaApi(accountId);
|
||||
case 'claude':
|
||||
@@ -336,13 +361,12 @@ async function fetchQuotaByProvider(
|
||||
* Supports agy, codex, claude, gemini, and ghcp providers
|
||||
*/
|
||||
export function useAccountQuota(provider: string, accountId: string, enabled = true) {
|
||||
const canonicalProvider = normalizeQuotaProvider(provider);
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['account-quota', provider, accountId],
|
||||
queryFn: () => fetchQuotaByProvider(provider, accountId),
|
||||
enabled:
|
||||
enabled &&
|
||||
QUOTA_SUPPORTED_PROVIDERS.includes(provider as QuotaSupportedProvider) &&
|
||||
!!accountId,
|
||||
queryKey: ['account-quota', canonicalProvider ?? provider, accountId],
|
||||
queryFn: () => fetchQuotaByProvider(canonicalProvider ?? provider, accountId),
|
||||
enabled: enabled && !!canonicalProvider && !!accountId,
|
||||
staleTime: 60000, // Match refetchInterval to prevent early refetching
|
||||
refetchInterval: 60000, // Refresh every 1 minute
|
||||
refetchOnWindowFocus: false, // Don't refetch on tab switch
|
||||
|
||||
+4
-2
@@ -670,8 +670,9 @@ export function getProviderMinQuota(
|
||||
quota: UnifiedQuotaResult | null | undefined
|
||||
): number | null {
|
||||
if (!quota?.success) return null;
|
||||
const normalizedProvider = provider.trim().toLowerCase();
|
||||
|
||||
switch (provider) {
|
||||
switch (normalizedProvider) {
|
||||
case 'agy':
|
||||
if (isAgyQuotaResult(quota)) {
|
||||
return getMinClaudeQuota(quota.models);
|
||||
@@ -713,8 +714,9 @@ export function getProviderResetTime(
|
||||
quota: UnifiedQuotaResult | null | undefined
|
||||
): string | null {
|
||||
if (!quota?.success) return null;
|
||||
const normalizedProvider = provider.trim().toLowerCase();
|
||||
|
||||
switch (provider) {
|
||||
switch (normalizedProvider) {
|
||||
case 'agy':
|
||||
if (isAgyQuotaResult(quota)) {
|
||||
return getClaudeResetTime(quota.models);
|
||||
|
||||
Reference in new issue
Block a user