mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 12:09:03 +00:00
353 lines
9.3 KiB
TypeScript
353 lines
9.3 KiB
TypeScript
/**
|
|
* React Query hook for CLIProxyAPI stats
|
|
*/
|
|
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import type {
|
|
ModelQuota,
|
|
QuotaResult,
|
|
CodexQuotaResult,
|
|
ClaudeQuotaResult,
|
|
GeminiCliQuotaResult,
|
|
GhcpQuotaResult,
|
|
} from '@/lib/api-client';
|
|
import type { UnifiedQuotaResult } from '@/lib/utils';
|
|
|
|
/** Per-account usage statistics */
|
|
export interface AccountUsageStats {
|
|
/** Account email or identifier */
|
|
source: string;
|
|
/** Number of successful requests */
|
|
successCount: number;
|
|
/** Number of failed requests */
|
|
failureCount: number;
|
|
/** Total tokens used */
|
|
totalTokens: number;
|
|
/** Last request timestamp */
|
|
lastUsedAt?: string;
|
|
}
|
|
|
|
/** CLIProxy usage statistics */
|
|
export interface CliproxyStats {
|
|
totalRequests: number;
|
|
successCount: number;
|
|
failureCount: number;
|
|
tokens: {
|
|
input: number;
|
|
output: number;
|
|
total: number;
|
|
};
|
|
requestsByModel: Record<string, number>;
|
|
requestsByProvider: Record<string, number>;
|
|
/** Per-account usage breakdown */
|
|
accountStats: Record<string, AccountUsageStats>;
|
|
quotaExceededCount: number;
|
|
retryCount: number;
|
|
collectedAt: string;
|
|
}
|
|
|
|
/** CLIProxy running status */
|
|
export interface CliproxyStatus {
|
|
running: boolean;
|
|
}
|
|
|
|
/**
|
|
* Fetch CLIProxy stats from API
|
|
*/
|
|
async function fetchCliproxyStats(): Promise<CliproxyStats> {
|
|
const response = await fetch('/api/cliproxy/stats');
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.message || 'Failed to fetch stats');
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
/**
|
|
* Fetch CLIProxy running status
|
|
*/
|
|
async function fetchCliproxyStatus(): Promise<CliproxyStatus> {
|
|
const response = await fetch('/api/cliproxy/status');
|
|
if (!response.ok) {
|
|
throw new Error('Failed to fetch status');
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
/**
|
|
* Hook to get CLIProxy running status
|
|
*/
|
|
export function useCliproxyStatus() {
|
|
return useQuery({
|
|
queryKey: ['cliproxy-status'],
|
|
queryFn: fetchCliproxyStatus,
|
|
refetchInterval: 10000, // Check every 10 seconds
|
|
retry: 1,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Hook to get CLIProxy usage stats
|
|
*/
|
|
export function useCliproxyStats(enabled = true) {
|
|
return useQuery({
|
|
queryKey: ['cliproxy-stats'],
|
|
queryFn: fetchCliproxyStats,
|
|
enabled,
|
|
refetchInterval: 5000, // Refresh every 5 seconds for near-real-time updates
|
|
retry: 1,
|
|
staleTime: 3000, // Consider data stale after 3 seconds
|
|
});
|
|
}
|
|
|
|
/** CLIProxy model from /v1/models endpoint */
|
|
export interface CliproxyModel {
|
|
id: string;
|
|
object: string;
|
|
created: number;
|
|
owned_by: string;
|
|
}
|
|
|
|
/** Categorized models response */
|
|
export interface CliproxyModelsResponse {
|
|
models: CliproxyModel[];
|
|
byCategory: Record<string, CliproxyModel[]>;
|
|
totalCount: number;
|
|
}
|
|
|
|
/**
|
|
* Fetch CLIProxy models from API
|
|
*/
|
|
async function fetchCliproxyModels(): Promise<CliproxyModelsResponse> {
|
|
const response = await fetch('/api/cliproxy/models');
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.message || 'Failed to fetch models');
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
/**
|
|
* Hook to get available CLIProxy models (categorized by provider)
|
|
*/
|
|
export function useCliproxyModels(enabled = true) {
|
|
return useQuery({
|
|
queryKey: ['cliproxy-models'],
|
|
queryFn: fetchCliproxyModels,
|
|
enabled,
|
|
staleTime: 60000, // Models don't change often, cache for 1 minute
|
|
retry: 1,
|
|
});
|
|
}
|
|
|
|
/** Error log file metadata from CLIProxyAPI */
|
|
export interface CliproxyErrorLog {
|
|
name: string;
|
|
size: number;
|
|
modified: number;
|
|
/** Absolute path to the log file (injected by backend) */
|
|
absolutePath?: string;
|
|
/** HTTP status code extracted from log (injected by backend) */
|
|
statusCode?: number;
|
|
/** Model name extracted from request body (injected by backend) */
|
|
model?: string;
|
|
}
|
|
|
|
/**
|
|
* Fetch CLIProxy error logs from API
|
|
*/
|
|
async function fetchCliproxyErrorLogs(): Promise<CliproxyErrorLog[]> {
|
|
const response = await fetch('/api/cliproxy/error-logs');
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.message || 'Failed to fetch error logs');
|
|
}
|
|
const data = await response.json();
|
|
return data.files ?? [];
|
|
}
|
|
|
|
/**
|
|
* Fetch specific error log content
|
|
*/
|
|
async function fetchCliproxyErrorLogContent(name: string): Promise<string> {
|
|
const response = await fetch(`/api/cliproxy/error-logs/${encodeURIComponent(name)}`);
|
|
if (!response.ok) {
|
|
throw new Error('Failed to fetch error log content');
|
|
}
|
|
return response.text();
|
|
}
|
|
|
|
/**
|
|
* Hook to get CLIProxy error logs list
|
|
*/
|
|
export function useCliproxyErrorLogs(enabled = true) {
|
|
return useQuery({
|
|
queryKey: ['cliproxy-error-logs'],
|
|
queryFn: fetchCliproxyErrorLogs,
|
|
enabled,
|
|
refetchInterval: 30000, // Refresh every 30 seconds
|
|
retry: 1,
|
|
staleTime: 10000,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Hook to get specific error log content
|
|
*/
|
|
export function useCliproxyErrorLogContent(name: string | null) {
|
|
return useQuery({
|
|
queryKey: ['cliproxy-error-log-content', name],
|
|
queryFn: () => (name ? fetchCliproxyErrorLogContent(name) : Promise.resolve('')),
|
|
enabled: !!name,
|
|
staleTime: 60000, // Cache log content for 1 minute
|
|
});
|
|
}
|
|
|
|
// Re-export for consumers
|
|
export type {
|
|
ModelQuota,
|
|
QuotaResult,
|
|
CodexQuotaResult,
|
|
ClaudeQuotaResult,
|
|
GeminiCliQuotaResult,
|
|
GhcpQuotaResult,
|
|
};
|
|
|
|
/** 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];
|
|
|
|
/**
|
|
* 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)}`);
|
|
if (!response.ok) {
|
|
let message = 'Failed to fetch 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 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 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
|
|
*/
|
|
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();
|
|
}
|
|
|
|
/**
|
|
* Fetch GitHub Copilot (ghcp) quota from API
|
|
*/
|
|
async function fetchGhcpQuotaApi(accountId: string): Promise<GhcpQuotaResult> {
|
|
const response = await fetch(`/api/cliproxy/quota/ghcp/${encodeURIComponent(accountId)}`);
|
|
if (!response.ok) {
|
|
let message = 'Failed to fetch GitHub Copilot 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();
|
|
}
|
|
|
|
// Re-export unified type from utils for consumers
|
|
export type { UnifiedQuotaResult } from '@/lib/utils';
|
|
|
|
/**
|
|
* Fetch quota by provider (dispatcher)
|
|
*/
|
|
async function fetchQuotaByProvider(
|
|
provider: string,
|
|
accountId: string
|
|
): Promise<UnifiedQuotaResult> {
|
|
switch (provider) {
|
|
case 'codex':
|
|
return fetchCodexQuotaApi(accountId);
|
|
case 'claude':
|
|
return fetchClaudeQuotaApi(accountId);
|
|
case 'gemini':
|
|
return fetchGeminiQuotaApi(accountId);
|
|
case 'ghcp':
|
|
return fetchGhcpQuotaApi(accountId);
|
|
default:
|
|
return fetchAccountQuota(provider, accountId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hook to get account quota
|
|
* Supports agy, codex, claude, gemini, and ghcp providers
|
|
*/
|
|
export function useAccountQuota(provider: string, accountId: string, enabled = true) {
|
|
return useQuery({
|
|
queryKey: ['account-quota', provider, accountId],
|
|
queryFn: () => fetchQuotaByProvider(provider, accountId),
|
|
enabled:
|
|
enabled &&
|
|
QUOTA_SUPPORTED_PROVIDERS.includes(provider as QuotaSupportedProvider) &&
|
|
!!accountId,
|
|
staleTime: 60000, // Match refetchInterval to prevent early refetching
|
|
refetchInterval: 60000, // Refresh every 1 minute
|
|
refetchOnWindowFocus: false, // Don't refetch on tab switch
|
|
refetchOnMount: false, // Don't refetch on component remount (AuthMonitor re-renders)
|
|
retry: 1,
|
|
});
|
|
}
|