mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 00:14:15 +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:
1 parent
2f5a50b801
commit
19a57c395c
3 files changed
+214
-6
No files matched your search
@@ -3,7 +3,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
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 */
|
/** Per-account usage statistics */
|
||||||
export interface AccountUsageStats {
|
export interface AccountUsageStats {
|
||||||
@@ -196,10 +201,13 @@ export function useCliproxyErrorLogContent(name: string | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Re-export for consumers
|
// 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> {
|
async function fetchAccountQuota(provider: string, accountId: string): Promise<QuotaResult> {
|
||||||
const response = await fetch(`/api/cliproxy/quota/${provider}/${encodeURIComponent(accountId)}`);
|
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();
|
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
|
* 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) {
|
export function useAccountQuota(provider: string, accountId: string, enabled = true) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['account-quota', provider, accountId],
|
queryKey: ['account-quota', provider, accountId],
|
||||||
queryFn: () => fetchAccountQuota(provider, accountId),
|
queryFn: () => fetchQuotaByProvider(provider, accountId),
|
||||||
enabled: enabled && provider === 'agy' && !!accountId,
|
enabled:
|
||||||
|
enabled &&
|
||||||
|
SUPPORTED_PROVIDERS.includes(provider as (typeof SUPPORTED_PROVIDERS)[number]) &&
|
||||||
|
!!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
|
||||||
refetchOnWindowFocus: false, // Don't refetch on tab switch
|
refetchOnWindowFocus: false, // Don't refetch on tab switch
|
||||||
@@ -232,3 +299,35 @@ export function useAccountQuota(provider: string, accountId: string, enabled = t
|
|||||||
retry: 1,
|
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;
|
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 */
|
/** Provider accounts summary */
|
||||||
export type ProviderAccountsMap = Record<string, OAuthAccount[]>;
|
export type ProviderAccountsMap = Record<string, OAuthAccount[]>;
|
||||||
|
|
||||||
@@ -552,5 +616,11 @@ export const api = {
|
|||||||
/** Fetch quota for a specific account */
|
/** Fetch quota for a specific account */
|
||||||
get: (provider: string, accountId: string) =>
|
get: (provider: string, accountId: string) =>
|
||||||
request<QuotaResult>(`/cliproxy/quota/${provider}/${encodeURIComponent(accountId)}`),
|
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 { clsx, type ClassValue } from 'clsx';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import type { CodexQuotaWindow, GeminiCliBucket } from './api-client';
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
@@ -298,3 +299,41 @@ export function groupModelsByTier(models: TieredModel[]): Map<ModelTier, TieredM
|
|||||||
}
|
}
|
||||||
return groups;
|
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