mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-20 02:17:05 +00:00
feat(cliproxy): add Claude quota windows and account failover
This commit is contained in:
1 parent
6a9abd2a48
commit
2385d9028a
19 files changed
+1599
-74
No files matched your search
@@ -8,6 +8,7 @@ import {
|
||||
getCodexQuotaBreakdown,
|
||||
getProviderMinQuota,
|
||||
getProviderResetTime,
|
||||
isClaudeQuotaResult,
|
||||
isCodexQuotaResult,
|
||||
} from '@/lib/utils';
|
||||
import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
|
||||
@@ -90,7 +91,7 @@ export function AccountCard({
|
||||
const borderColor = getBorderColorStyle(zone, account.color);
|
||||
const connectorPosition = CONNECTOR_POSITION_MAP[zone];
|
||||
|
||||
// Quota for CLIProxy accounts (agy, codex, gemini)
|
||||
// Quota for CLIProxy accounts (agy, codex, claude, gemini, ghcp)
|
||||
const isCliproxyProvider = QUOTA_SUPPORTED_PROVIDERS.includes(
|
||||
account.provider as QuotaSupportedProvider
|
||||
);
|
||||
@@ -111,6 +112,40 @@ export function AccountCard({
|
||||
{ label: '5h', value: codexBreakdown?.fiveHourWindow?.remainingPercent ?? null },
|
||||
{ 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)
|
||||
? [
|
||||
{
|
||||
label: '5h',
|
||||
value:
|
||||
quota.coreUsage?.fiveHour?.remainingPercent ??
|
||||
quota.windows.find((window) => window.rateLimitType === 'five_hour')
|
||||
?.remainingPercent ??
|
||||
null,
|
||||
},
|
||||
{
|
||||
label: 'Wk',
|
||||
value:
|
||||
quota.coreUsage?.weekly?.remainingPercent ??
|
||||
quota.windows.find((window) =>
|
||||
[
|
||||
'seven_day',
|
||||
'seven_day_opus',
|
||||
'seven_day_sonnet',
|
||||
'seven_day_oauth_apps',
|
||||
'seven_day_cowork',
|
||||
].includes(window.rateLimitType)
|
||||
)?.remainingPercent ??
|
||||
null,
|
||||
},
|
||||
].filter((row): row is { label: string; value: number } => row.value !== null)
|
||||
: [];
|
||||
const compactQuotaRows =
|
||||
account.provider === 'codex'
|
||||
? codexQuotaRows
|
||||
: account.provider === 'claude'
|
||||
? claudeQuotaRows
|
||||
: [];
|
||||
const minQuotaLabel = minQuota !== null ? formatQuotaPercent(minQuota) : null;
|
||||
|
||||
// Tier badge (AGY only) - show P for Pro, U for Ultra
|
||||
@@ -215,7 +250,7 @@ export function AccountCard({
|
||||
failure={account.failureCount}
|
||||
showDetails={showDetails}
|
||||
/>
|
||||
{/* Quota bar for CLIProxy accounts (agy, codex, gemini) */}
|
||||
{/* Quota bar for CLIProxy accounts */}
|
||||
{isCliproxyProvider && (
|
||||
<div className="mt-2 px-0.5">
|
||||
{quotaLoading ? (
|
||||
@@ -245,9 +280,9 @@ export function AccountCard({
|
||||
{minQuotaLabel}%
|
||||
</span>
|
||||
</div>
|
||||
{account.provider === 'codex' && codexQuotaRows.length > 0 && (
|
||||
{compactQuotaRows.length > 0 && (
|
||||
<div className="flex items-center justify-between text-[7px] text-muted-foreground/70">
|
||||
{codexQuotaRows.map((row) => (
|
||||
{compactQuotaRows.map((row) => (
|
||||
<span key={row.label}>
|
||||
{row.label} {row.value}%
|
||||
</span>
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
getCodexQuotaBreakdown,
|
||||
getProviderMinQuota,
|
||||
getProviderResetTime,
|
||||
isClaudeQuotaResult,
|
||||
isCodexQuotaResult,
|
||||
} from '@/lib/utils';
|
||||
import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
|
||||
@@ -130,6 +131,40 @@ export function AccountItem({
|
||||
{ label: '5h', value: codexBreakdown?.fiveHourWindow?.remainingPercent ?? null },
|
||||
{ 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)
|
||||
? [
|
||||
{
|
||||
label: '5h',
|
||||
value:
|
||||
quota.coreUsage?.fiveHour?.remainingPercent ??
|
||||
quota.windows.find((window) => window.rateLimitType === 'five_hour')
|
||||
?.remainingPercent ??
|
||||
null,
|
||||
},
|
||||
{
|
||||
label: 'Weekly',
|
||||
value:
|
||||
quota.coreUsage?.weekly?.remainingPercent ??
|
||||
quota.windows.find((window) =>
|
||||
[
|
||||
'seven_day',
|
||||
'seven_day_opus',
|
||||
'seven_day_sonnet',
|
||||
'seven_day_oauth_apps',
|
||||
'seven_day_cowork',
|
||||
].includes(window.rateLimitType)
|
||||
)?.remainingPercent ??
|
||||
null,
|
||||
},
|
||||
].filter((row): row is { label: string; value: number } => row.value !== null)
|
||||
: [];
|
||||
const dualWindowQuotaRows =
|
||||
account.provider === 'codex'
|
||||
? codexQuotaRows
|
||||
: account.provider === 'claude'
|
||||
? claudeQuotaRows
|
||||
: [];
|
||||
const minQuotaLabel = minQuota !== null ? formatQuotaPercent(minQuota) : null;
|
||||
|
||||
return (
|
||||
@@ -353,9 +388,9 @@ export function AccountItem({
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
{account.provider === 'codex' && codexQuotaRows.length > 0 ? (
|
||||
{dualWindowQuotaRows.length > 0 ? (
|
||||
<div className="space-y-1.5">
|
||||
{codexQuotaRows.map((row) => (
|
||||
{dualWindowQuotaRows.map((row) => (
|
||||
<div key={row.label} className="flex items-center gap-2">
|
||||
<span className="w-10 text-[10px] text-muted-foreground">
|
||||
{row.label}
|
||||
|
||||
@@ -35,7 +35,7 @@ interface AccountsSectionProps {
|
||||
/** Bulk resume mutation in progress */
|
||||
isBulkResuming?: boolean;
|
||||
privacyMode?: boolean;
|
||||
/** Show quota bars for accounts (only applicable for 'agy' provider) */
|
||||
/** Show quota bars for accounts when provider supports quota API */
|
||||
showQuota?: boolean;
|
||||
/** Kiro-specific: show "use normal browser" toggle */
|
||||
isKiro?: boolean;
|
||||
|
||||
@@ -61,7 +61,7 @@ export interface AccountItemProps {
|
||||
/** Solo mode mutation in progress */
|
||||
isSoloingAccount?: boolean;
|
||||
privacyMode?: boolean;
|
||||
/** Show quota bar (only for 'agy' provider) */
|
||||
/** Show quota bar for providers with quota API support */
|
||||
showQuota?: boolean;
|
||||
/** Enable checkbox for multi-select */
|
||||
selectable?: boolean;
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
getModelsWithTiers,
|
||||
groupModelsByTier,
|
||||
isAgyQuotaResult,
|
||||
isClaudeQuotaResult,
|
||||
isCodexQuotaResult,
|
||||
isGeminiQuotaResult,
|
||||
isGhcpQuotaResult,
|
||||
@@ -35,6 +36,27 @@ function formatPlanLabel(planType: string | null | undefined): string | null {
|
||||
return normalized.length > 0 ? normalized.join(' ') : planType;
|
||||
}
|
||||
|
||||
function getClaudeWindowDisplayLabel(rateLimitType: string, fallback: string): string {
|
||||
switch (rateLimitType) {
|
||||
case 'five_hour':
|
||||
return '5h usage limit';
|
||||
case 'seven_day':
|
||||
return 'Weekly usage limit';
|
||||
case 'seven_day_opus':
|
||||
return 'Weekly usage (Opus)';
|
||||
case 'seven_day_sonnet':
|
||||
return 'Weekly usage (Sonnet)';
|
||||
case 'seven_day_oauth_apps':
|
||||
return 'Weekly usage (OAuth apps)';
|
||||
case 'seven_day_cowork':
|
||||
return 'Weekly usage (Cowork)';
|
||||
case 'overage':
|
||||
return 'Extra usage';
|
||||
default:
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders provider-specific quota tooltip content
|
||||
* Uses type guards for proper TypeScript narrowing
|
||||
@@ -115,6 +137,74 @@ export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentPro
|
||||
);
|
||||
}
|
||||
|
||||
// Claude provider tooltip
|
||||
if (isClaudeQuotaResult(quota)) {
|
||||
const coreWindows = [quota.coreUsage?.fiveHour, quota.coreUsage?.weekly]
|
||||
.filter((window): window is NonNullable<typeof window> => !!window)
|
||||
.map((window) => ({
|
||||
rateLimitType: window.rateLimitType,
|
||||
label: window.label,
|
||||
remainingPercent: window.remainingPercent,
|
||||
resetAt: window.resetAt,
|
||||
status: window.status,
|
||||
}));
|
||||
const policyWindows = quota.windows.map((window) => ({
|
||||
rateLimitType: window.rateLimitType,
|
||||
label: window.label,
|
||||
remainingPercent: window.remainingPercent,
|
||||
resetAt: window.resetAt,
|
||||
status: window.status,
|
||||
}));
|
||||
const orderedWindows = [...coreWindows, ...policyWindows].filter(
|
||||
(window, index, arr) =>
|
||||
arr.findIndex(
|
||||
(candidate) =>
|
||||
candidate.rateLimitType === window.rateLimitType &&
|
||||
candidate.resetAt === window.resetAt &&
|
||||
candidate.status === window.status
|
||||
) === index
|
||||
);
|
||||
|
||||
const fiveHourResetAt =
|
||||
quota.coreUsage?.fiveHour?.resetAt ??
|
||||
quota.windows.find((window) => window.rateLimitType === 'five_hour')?.resetAt ??
|
||||
null;
|
||||
const weeklyResetAt =
|
||||
quota.coreUsage?.weekly?.resetAt ??
|
||||
quota.windows.find((window) =>
|
||||
[
|
||||
'seven_day',
|
||||
'seven_day_opus',
|
||||
'seven_day_sonnet',
|
||||
'seven_day_oauth_apps',
|
||||
'seven_day_cowork',
|
||||
].includes(window.rateLimitType)
|
||||
)?.resetAt ??
|
||||
null;
|
||||
|
||||
return (
|
||||
<div className="text-xs space-y-1">
|
||||
<p className="font-medium">Rate Limits:</p>
|
||||
{orderedWindows.map((window, index) => (
|
||||
<div
|
||||
key={`${window.rateLimitType}-${window.resetAt ?? 'no-reset'}-${window.status}-${index}`}
|
||||
className="flex justify-between gap-4"
|
||||
>
|
||||
<span className={cn(window.remainingPercent < 20 && 'text-red-500')}>
|
||||
{getClaudeWindowDisplayLabel(window.rateLimitType, window.label)}
|
||||
</span>
|
||||
<span className="font-mono">{window.remainingPercent}%</span>
|
||||
</div>
|
||||
))}
|
||||
<CodexResetIndicators
|
||||
fiveHourResetTime={fiveHourResetAt}
|
||||
weeklyResetTime={weeklyResetAt}
|
||||
fallbackResetTime={resetTime}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Gemini provider tooltip
|
||||
if (isGeminiQuotaResult(quota)) {
|
||||
return (
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
ModelQuota,
|
||||
QuotaResult,
|
||||
CodexQuotaResult,
|
||||
ClaudeQuotaResult,
|
||||
GeminiCliQuotaResult,
|
||||
GhcpQuotaResult,
|
||||
} from '@/lib/api-client';
|
||||
@@ -203,14 +204,21 @@ export function useCliproxyErrorLogContent(name: string | null) {
|
||||
}
|
||||
|
||||
// Re-export for consumers
|
||||
export type { ModelQuota, QuotaResult, CodexQuotaResult, GeminiCliQuotaResult, GhcpQuotaResult };
|
||||
export type {
|
||||
ModelQuota,
|
||||
QuotaResult,
|
||||
CodexQuotaResult,
|
||||
ClaudeQuotaResult,
|
||||
GeminiCliQuotaResult,
|
||||
GhcpQuotaResult,
|
||||
};
|
||||
|
||||
/** Providers with quota API support */
|
||||
export const QUOTA_SUPPORTED_PROVIDERS = ['agy', 'codex', 'gemini', 'ghcp'] as const;
|
||||
export const QUOTA_SUPPORTED_PROVIDERS = ['agy', 'codex', 'claude', 'gemini', 'ghcp'] as const;
|
||||
export type QuotaSupportedProvider = (typeof QUOTA_SUPPORTED_PROVIDERS)[number];
|
||||
|
||||
/**
|
||||
* Fetch account quota from API (Antigravity only)
|
||||
* Fetch account quota from generic API route
|
||||
*/
|
||||
async function fetchAccountQuota(provider: string, accountId: string): Promise<QuotaResult> {
|
||||
const response = await fetch(`/api/cliproxy/quota/${provider}/${encodeURIComponent(accountId)}`);
|
||||
@@ -245,6 +253,24 @@ async function fetchCodexQuotaApi(accountId: string): Promise<CodexQuotaResult>
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch Claude quota from API
|
||||
*/
|
||||
async function fetchClaudeQuotaApi(accountId: string): Promise<ClaudeQuotaResult> {
|
||||
const response = await fetch(`/api/cliproxy/quota/claude/${encodeURIComponent(accountId)}`);
|
||||
if (!response.ok) {
|
||||
let message = 'Failed to fetch Claude quota';
|
||||
try {
|
||||
const error = await response.json();
|
||||
message = error.message || message;
|
||||
} catch {
|
||||
// Use default message if response isn't JSON
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch Gemini quota from API
|
||||
*/
|
||||
@@ -294,6 +320,8 @@ async function fetchQuotaByProvider(
|
||||
switch (provider) {
|
||||
case 'codex':
|
||||
return fetchCodexQuotaApi(accountId);
|
||||
case 'claude':
|
||||
return fetchClaudeQuotaApi(accountId);
|
||||
case 'gemini':
|
||||
return fetchGeminiQuotaApi(accountId);
|
||||
case 'ghcp':
|
||||
@@ -305,7 +333,7 @@ async function fetchQuotaByProvider(
|
||||
|
||||
/**
|
||||
* Hook to get account quota
|
||||
* Supports agy, codex, gemini, and ghcp providers
|
||||
* Supports agy, codex, claude, gemini, and ghcp providers
|
||||
*/
|
||||
export function useAccountQuota(provider: string, accountId: string, enabled = true) {
|
||||
return useQuery({
|
||||
|
||||
@@ -299,6 +299,59 @@ export interface CodexQuotaResult {
|
||||
isForbidden?: boolean;
|
||||
}
|
||||
|
||||
/** Claude policy limit window */
|
||||
export interface ClaudeQuotaWindow {
|
||||
/** Source identifier: five_hour, seven_day, seven_day_opus, seven_day_sonnet, overage, ... */
|
||||
rateLimitType: string;
|
||||
/** Human-friendly label for UI display */
|
||||
label: string;
|
||||
/** Upstream status: allowed, allowed_warning, rejected */
|
||||
status: string;
|
||||
/** Utilization ratio (0-1) when available */
|
||||
utilization: number | null;
|
||||
/** Utilization as percentage (0-100) */
|
||||
usedPercent: number;
|
||||
/** Remaining percentage (100 - usedPercent) */
|
||||
remainingPercent: number;
|
||||
/** Reset timestamp for this window, null if unknown */
|
||||
resetAt: string | null;
|
||||
surpassedThreshold?: boolean;
|
||||
severity?: string;
|
||||
overageStatus?: string;
|
||||
overageResetsAt?: string | null;
|
||||
overageDisabledReason?: string | null;
|
||||
isUsingOverage?: boolean;
|
||||
hasExtraUsageEnabled?: boolean;
|
||||
}
|
||||
|
||||
/** Core Claude usage window (5h/weekly) */
|
||||
export interface ClaudeCoreUsageWindow {
|
||||
rateLimitType: string;
|
||||
label: string;
|
||||
remainingPercent: number;
|
||||
resetAt: string | null;
|
||||
status: string;
|
||||
}
|
||||
|
||||
/** Core Claude usage summary (5h + weekly) */
|
||||
export interface ClaudeCoreUsageSummary {
|
||||
fiveHour: ClaudeCoreUsageWindow | null;
|
||||
weekly: ClaudeCoreUsageWindow | null;
|
||||
}
|
||||
|
||||
/** Claude quota result */
|
||||
export interface ClaudeQuotaResult {
|
||||
success: boolean;
|
||||
windows: ClaudeQuotaWindow[];
|
||||
coreUsage?: ClaudeCoreUsageSummary;
|
||||
lastUpdated: number;
|
||||
error?: string;
|
||||
accountId?: string;
|
||||
needsReauth?: boolean;
|
||||
/** True if result was served from cache */
|
||||
cached?: boolean;
|
||||
}
|
||||
|
||||
/** Gemini CLI bucket (grouped by model series) */
|
||||
export interface GeminiCliBucket {
|
||||
/** Unique bucket identifier (e.g., "gemini-flash-series::input") */
|
||||
@@ -793,6 +846,9 @@ export const api = {
|
||||
/** Fetch Codex quota for a specific account */
|
||||
getCodex: (accountId: string) =>
|
||||
request<CodexQuotaResult>(`/cliproxy/quota/codex/${encodeURIComponent(accountId)}`),
|
||||
/** Fetch Claude quota for a specific account */
|
||||
getClaude: (accountId: string) =>
|
||||
request<ClaudeQuotaResult>(`/cliproxy/quota/claude/${encodeURIComponent(accountId)}`),
|
||||
/** Fetch Gemini CLI quota for a specific account */
|
||||
getGemini: (accountId: string) =>
|
||||
request<GeminiCliQuotaResult>(`/cliproxy/quota/gemini/${encodeURIComponent(accountId)}`),
|
||||
|
||||
+69
-1
@@ -3,6 +3,7 @@ import { twMerge } from 'tailwind-merge';
|
||||
import type {
|
||||
CodexQuotaWindow,
|
||||
CodexQuotaResult,
|
||||
ClaudeQuotaResult,
|
||||
GeminiCliBucket,
|
||||
GeminiCliQuotaResult,
|
||||
GhcpQuotaResult,
|
||||
@@ -42,6 +43,7 @@ const PROVIDER_COLORS: Record<string, string> = {
|
||||
agy: '#f3722c', // Pumpkin
|
||||
gemini: '#277da1', // Cerulean
|
||||
codex: '#f8961e', // Carrot
|
||||
claude: '#4d908e', // Dark Cyan
|
||||
vertex: '#577590', // Blue Slate
|
||||
iflow: '#f94144', // Strawberry
|
||||
qwen: '#f9c74f', // Tuscan
|
||||
@@ -522,6 +524,48 @@ export function getCodexResetTime(windows: CodexQuotaWindow[]): string | null {
|
||||
return resets.sort()[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get minimum remaining percentage across Claude policy windows.
|
||||
*/
|
||||
export function getMinClaudePolicyQuota(quota: ClaudeQuotaResult): number | null {
|
||||
if (!quota.success) return null;
|
||||
|
||||
const coreWindows = [quota.coreUsage?.fiveHour, quota.coreUsage?.weekly].filter(
|
||||
(window): window is NonNullable<typeof window> => !!window
|
||||
);
|
||||
if (coreWindows.length > 0) {
|
||||
return Math.min(...coreWindows.map((window) => window.remainingPercent));
|
||||
}
|
||||
|
||||
const usageWindows = quota.windows.filter((window) => window.rateLimitType !== 'overage');
|
||||
if (usageWindows.length > 0) {
|
||||
return Math.min(...usageWindows.map((window) => window.remainingPercent));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get earliest reset time from Claude policy windows.
|
||||
*/
|
||||
export function getClaudePolicyResetTime(quota: ClaudeQuotaResult): string | null {
|
||||
if (!quota.success) return null;
|
||||
|
||||
const coreResets = [quota.coreUsage?.fiveHour?.resetAt, quota.coreUsage?.weekly?.resetAt].filter(
|
||||
(value): value is string => !!value
|
||||
);
|
||||
if (coreResets.length > 0) {
|
||||
return coreResets.sort()[0];
|
||||
}
|
||||
|
||||
const resets = quota.windows
|
||||
.filter((window) => window.rateLimitType !== 'overage')
|
||||
.map((window) => window.resetAt)
|
||||
.filter((value): value is string => value !== null);
|
||||
if (resets.length === 0) return null;
|
||||
return resets.sort()[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get minimum remaining percentage across Gemini CLI buckets
|
||||
*/
|
||||
@@ -570,6 +614,7 @@ export function getGhcpResetTime(quotaResetDate: string | null): string | null {
|
||||
export type UnifiedQuotaResult =
|
||||
| QuotaResult
|
||||
| CodexQuotaResult
|
||||
| ClaudeQuotaResult
|
||||
| GeminiCliQuotaResult
|
||||
| GhcpQuotaResult;
|
||||
|
||||
@@ -580,7 +625,18 @@ export function isAgyQuotaResult(quota: UnifiedQuotaResult): quota is QuotaResul
|
||||
|
||||
/** 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);
|
||||
return (
|
||||
'windows' in quota && 'planType' in quota && Array.isArray((quota as CodexQuotaResult).windows)
|
||||
);
|
||||
}
|
||||
|
||||
/** Type guard: Check if quota result is from Claude provider */
|
||||
export function isClaudeQuotaResult(quota: UnifiedQuotaResult): quota is ClaudeQuotaResult {
|
||||
return (
|
||||
'windows' in quota &&
|
||||
!('planType' in quota) &&
|
||||
Array.isArray((quota as ClaudeQuotaResult).windows)
|
||||
);
|
||||
}
|
||||
|
||||
/** Type guard: Check if quota result is from Gemini CLI provider */
|
||||
@@ -626,6 +682,12 @@ export function getProviderMinQuota(
|
||||
return getMinCodexQuota(quota.windows);
|
||||
}
|
||||
return null;
|
||||
case 'claude':
|
||||
case 'anthropic':
|
||||
if (isClaudeQuotaResult(quota)) {
|
||||
return getMinClaudePolicyQuota(quota);
|
||||
}
|
||||
return null;
|
||||
case 'gemini':
|
||||
if (isGeminiQuotaResult(quota)) {
|
||||
return getMinGeminiQuota(quota.buckets);
|
||||
@@ -663,6 +725,12 @@ export function getProviderResetTime(
|
||||
return getCodexResetTime(quota.windows);
|
||||
}
|
||||
return null;
|
||||
case 'claude':
|
||||
case 'anthropic':
|
||||
if (isClaudeQuotaResult(quota)) {
|
||||
return getClaudePolicyResetTime(quota);
|
||||
}
|
||||
return null;
|
||||
case 'gemini':
|
||||
if (isGeminiQuotaResult(quota)) {
|
||||
return getGeminiResetTime(quota.buckets);
|
||||
|
||||
Reference in new issue
Block a user