mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 08:17:22 +00:00
feat(ui): add Codex/Gemini quota API client and hooks
- add CodexQuotaResult and GeminiCliQuotaResult types - add api.quota.getCodex and api.quota.getGemini API functions - add useCodexQuota and useGeminiQuota React hooks - add getMinCodexQuota, getMinGeminiQuota, getCodexResetTime, getGeminiResetTime utils
This commit is contained in:
@@ -3,7 +3,12 @@
|
||||
*/
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { ModelQuota, QuotaResult } from '@/lib/api-client';
|
||||
import type {
|
||||
ModelQuota,
|
||||
QuotaResult,
|
||||
CodexQuotaResult,
|
||||
GeminiCliQuotaResult,
|
||||
} from '@/lib/api-client';
|
||||
|
||||
/** Per-account usage statistics */
|
||||
export interface AccountUsageStats {
|
||||
@@ -196,10 +201,13 @@ export function useCliproxyErrorLogContent(name: string | null) {
|
||||
}
|
||||
|
||||
// Re-export for consumers
|
||||
export type { ModelQuota, QuotaResult };
|
||||
export type { ModelQuota, QuotaResult, CodexQuotaResult, GeminiCliQuotaResult };
|
||||
|
||||
/** Providers with quota API support */
|
||||
const SUPPORTED_PROVIDERS = ['agy', 'codex', 'gemini'] as const;
|
||||
|
||||
/**
|
||||
* Fetch account quota from API
|
||||
* Fetch account quota from API (Antigravity only)
|
||||
*/
|
||||
async function fetchAccountQuota(provider: string, accountId: string): Promise<QuotaResult> {
|
||||
const response = await fetch(`/api/cliproxy/quota/${provider}/${encodeURIComponent(accountId)}`);
|
||||
@@ -216,15 +224,74 @@ async function fetchAccountQuota(provider: string, accountId: string): Promise<Q
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch Codex quota from API
|
||||
*/
|
||||
async function fetchCodexQuotaApi(accountId: string): Promise<CodexQuotaResult> {
|
||||
const response = await fetch(`/api/cliproxy/quota/codex/${encodeURIComponent(accountId)}`);
|
||||
if (!response.ok) {
|
||||
let message = 'Failed to fetch Codex 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
|
||||
*/
|
||||
async function fetchGeminiQuotaApi(accountId: string): Promise<GeminiCliQuotaResult> {
|
||||
const response = await fetch(`/api/cliproxy/quota/gemini/${encodeURIComponent(accountId)}`);
|
||||
if (!response.ok) {
|
||||
let message = 'Failed to fetch Gemini 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();
|
||||
}
|
||||
|
||||
/** Unified quota result type for all providers */
|
||||
export type UnifiedQuotaResult = QuotaResult | CodexQuotaResult | GeminiCliQuotaResult;
|
||||
|
||||
/**
|
||||
* Fetch quota by provider (dispatcher)
|
||||
*/
|
||||
async function fetchQuotaByProvider(
|
||||
provider: string,
|
||||
accountId: string
|
||||
): Promise<UnifiedQuotaResult> {
|
||||
switch (provider) {
|
||||
case 'codex':
|
||||
return fetchCodexQuotaApi(accountId);
|
||||
case 'gemini':
|
||||
return fetchGeminiQuotaApi(accountId);
|
||||
default:
|
||||
return fetchAccountQuota(provider, accountId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get account quota
|
||||
* Supports all providers that have quota API implemented
|
||||
* Supports agy, codex, and gemini providers
|
||||
*/
|
||||
export function useAccountQuota(provider: string, accountId: string, enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: ['account-quota', provider, accountId],
|
||||
queryFn: () => fetchAccountQuota(provider, accountId),
|
||||
enabled: enabled && provider === 'agy' && !!accountId,
|
||||
queryFn: () => fetchQuotaByProvider(provider, accountId),
|
||||
enabled:
|
||||
enabled &&
|
||||
SUPPORTED_PROVIDERS.includes(provider as (typeof SUPPORTED_PROVIDERS)[number]) &&
|
||||
!!accountId,
|
||||
staleTime: 60000, // Match refetchInterval to prevent early refetching
|
||||
refetchInterval: 60000, // Refresh every 1 minute
|
||||
refetchOnWindowFocus: false, // Don't refetch on tab switch
|
||||
@@ -232,3 +299,35 @@ export function useAccountQuota(provider: string, accountId: string, enabled = t
|
||||
retry: 1,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get Codex quota for a specific account
|
||||
*/
|
||||
export function useCodexQuota(accountId: string | null) {
|
||||
return useQuery({
|
||||
queryKey: ['codex-quota', accountId],
|
||||
queryFn: async () => {
|
||||
if (!accountId) throw new Error('Account ID required');
|
||||
return fetchCodexQuotaApi(accountId);
|
||||
},
|
||||
enabled: !!accountId,
|
||||
staleTime: 30000,
|
||||
refetchInterval: 60000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get Gemini CLI quota for a specific account
|
||||
*/
|
||||
export function useGeminiQuota(accountId: string | null) {
|
||||
return useQuery({
|
||||
queryKey: ['gemini-quota', accountId],
|
||||
queryFn: async () => {
|
||||
if (!accountId) throw new Error('Account ID required');
|
||||
return fetchGeminiQuotaApi(accountId);
|
||||
},
|
||||
enabled: !!accountId,
|
||||
staleTime: 30000,
|
||||
refetchInterval: 60000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -146,6 +146,70 @@ export interface QuotaResult {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/** Codex rate limit window */
|
||||
export interface CodexQuotaWindow {
|
||||
/** Window label: "Primary", "Secondary", "Code Review (Primary)", "Code Review (Secondary)" */
|
||||
label: string;
|
||||
/** Percentage used (0-100) */
|
||||
usedPercent: number;
|
||||
/** Percentage remaining (100 - usedPercent) */
|
||||
remainingPercent: number;
|
||||
/** Seconds until quota resets, null if unknown */
|
||||
resetAfterSeconds: number | null;
|
||||
/** ISO timestamp when quota resets, null if unknown */
|
||||
resetAt: string | null;
|
||||
}
|
||||
|
||||
/** Codex quota result */
|
||||
export interface CodexQuotaResult {
|
||||
/** Whether fetch succeeded */
|
||||
success: boolean;
|
||||
/** Quota windows (primary, secondary, code review) */
|
||||
windows: CodexQuotaWindow[];
|
||||
/** Plan type: free, plus, team, or null if unknown */
|
||||
planType: 'free' | 'plus' | 'team' | null;
|
||||
/** Timestamp of fetch */
|
||||
lastUpdated: number;
|
||||
/** Error message if fetch failed */
|
||||
error?: string;
|
||||
/** Account ID (email) this quota belongs to */
|
||||
accountId?: string;
|
||||
}
|
||||
|
||||
/** Gemini CLI bucket (grouped by model series) */
|
||||
export interface GeminiCliBucket {
|
||||
/** Unique bucket identifier (e.g., "gemini-flash-series::input") */
|
||||
id: string;
|
||||
/** Display label (e.g., "Gemini Flash Series") */
|
||||
label: string;
|
||||
/** Token type: "input", "output", or null if combined */
|
||||
tokenType: string | null;
|
||||
/** Remaining quota as fraction (0-1) */
|
||||
remainingFraction: number;
|
||||
/** Remaining quota as percentage (0-100) */
|
||||
remainingPercent: number;
|
||||
/** ISO timestamp when quota resets, null if unknown */
|
||||
resetTime: string | null;
|
||||
/** Model IDs in this bucket */
|
||||
modelIds: string[];
|
||||
}
|
||||
|
||||
/** Gemini CLI quota result */
|
||||
export interface GeminiCliQuotaResult {
|
||||
/** Whether fetch succeeded */
|
||||
success: boolean;
|
||||
/** Quota buckets grouped by model series */
|
||||
buckets: GeminiCliBucket[];
|
||||
/** GCP project ID for this account */
|
||||
projectId: string | null;
|
||||
/** Timestamp of fetch */
|
||||
lastUpdated: number;
|
||||
/** Error message if fetch failed */
|
||||
error?: string;
|
||||
/** Account ID (email) this quota belongs to */
|
||||
accountId?: string;
|
||||
}
|
||||
|
||||
/** Provider accounts summary */
|
||||
export type ProviderAccountsMap = Record<string, OAuthAccount[]>;
|
||||
|
||||
@@ -552,5 +616,11 @@ export const api = {
|
||||
/** Fetch quota for a specific account */
|
||||
get: (provider: string, accountId: string) =>
|
||||
request<QuotaResult>(`/cliproxy/quota/${provider}/${encodeURIComponent(accountId)}`),
|
||||
/** Fetch Codex quota for a specific account */
|
||||
getCodex: (accountId: string) =>
|
||||
request<CodexQuotaResult>(`/cliproxy/quota/codex/${encodeURIComponent(accountId)}`),
|
||||
/** Fetch Gemini CLI quota for a specific account */
|
||||
getGemini: (accountId: string) =>
|
||||
request<GeminiCliQuotaResult>(`/cliproxy/quota/gemini/${encodeURIComponent(accountId)}`),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import type { CodexQuotaWindow, GeminiCliBucket } from './api-client';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
@@ -298,3 +299,41 @@ export function groupModelsByTier(models: TieredModel[]): Map<ModelTier, TieredM
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get minimum remaining percentage across Codex rate limit windows
|
||||
*/
|
||||
export function getMinCodexQuota(windows: CodexQuotaWindow[]): number | null {
|
||||
if (!windows || windows.length === 0) return null;
|
||||
const percentages = windows.map((w) => w.remainingPercent);
|
||||
return Math.min(...percentages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get earliest reset time from Codex windows
|
||||
*/
|
||||
export function getCodexResetTime(windows: CodexQuotaWindow[]): string | null {
|
||||
if (!windows || windows.length === 0) return null;
|
||||
const resets = windows.map((w) => w.resetAt).filter((t): t is string => t !== null);
|
||||
if (resets.length === 0) return null;
|
||||
return resets.sort()[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get minimum remaining percentage across Gemini CLI buckets
|
||||
*/
|
||||
export function getMinGeminiQuota(buckets: GeminiCliBucket[]): number | null {
|
||||
if (!buckets || buckets.length === 0) return null;
|
||||
const percentages = buckets.map((b) => b.remainingPercent);
|
||||
return Math.min(...percentages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get earliest reset time from Gemini buckets
|
||||
*/
|
||||
export function getGeminiResetTime(buckets: GeminiCliBucket[]): string | null {
|
||||
if (!buckets || buckets.length === 0) return null;
|
||||
const resets = buckets.map((b) => b.resetTime).filter((t): t is string => t !== null);
|
||||
if (resets.length === 0) return null;
|
||||
return resets.sort()[0];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user