Spaces:
Sleeping
Sleeping
| import { getGlobalManagementKey } from '@/components/AuthContext'; | |
| const API_BASE = '/api'; | |
| // Types matching backend structures | |
| export interface GeminiKey { | |
| 'api-key': string; | |
| prefix?: string; | |
| 'base-url'?: string; | |
| 'proxy-url'?: string; | |
| headers?: Record<string, string>; | |
| 'excluded-models'?: string[]; | |
| } | |
| export interface ClaudeModel { | |
| name: string; | |
| alias?: string; | |
| } | |
| export interface ClaudeKey { | |
| 'api-key': string; | |
| prefix?: string; | |
| 'base-url'?: string; | |
| 'proxy-url'?: string; | |
| models?: ClaudeModel[]; | |
| headers?: Record<string, string>; | |
| 'excluded-models'?: string[]; | |
| } | |
| export interface CodexModel { | |
| name: string; | |
| alias?: string; | |
| } | |
| export interface CodexKey { | |
| 'api-key': string; | |
| prefix?: string; | |
| 'base-url'?: string; | |
| 'proxy-url'?: string; | |
| models?: CodexModel[]; | |
| headers?: Record<string, string>; | |
| 'excluded-models'?: string[]; | |
| } | |
| export interface VertexCompatModel { | |
| name: string; | |
| alias: string; | |
| } | |
| export interface VertexCompatKey { | |
| 'api-key': string; | |
| prefix?: string; | |
| 'base-url'?: string; | |
| 'proxy-url'?: string; | |
| headers?: Record<string, string>; | |
| models?: VertexCompatModel[]; | |
| } | |
| export interface OpenAICompatibilityAPIKey { | |
| 'api-key': string; | |
| prefix?: string; | |
| } | |
| export interface OpenAICompatibilityModel { | |
| name: string; | |
| alias?: string; | |
| } | |
| export interface OpenAICompatibility { | |
| name: string; | |
| prefix?: string; | |
| 'base-url': string; | |
| 'api-key-entries'?: OpenAICompatibilityAPIKey[]; | |
| models?: OpenAICompatibilityModel[]; | |
| headers?: Record<string, string>; | |
| } | |
| export interface OAuthModelAlias { | |
| from: string; | |
| to: string; | |
| } | |
| // Usage statistics types - matching backend structure | |
| export interface TokenStats { | |
| input_tokens: number; | |
| output_tokens: number; | |
| reasoning_tokens: number; | |
| cached_tokens: number; | |
| total_tokens: number; | |
| } | |
| export interface RequestDetail { | |
| timestamp: string; | |
| source: string; | |
| auth_index: string; | |
| tokens: TokenStats; | |
| failed: boolean; | |
| } | |
| export interface ModelSnapshot { | |
| total_requests: number; | |
| total_tokens: number; | |
| details?: RequestDetail[]; | |
| } | |
| export interface APISnapshot { | |
| total_requests: number; | |
| total_tokens: number; | |
| models: Record<string, ModelSnapshot>; | |
| } | |
| export interface UsageSnapshot { | |
| total_requests: number; | |
| success_count: number; | |
| failure_count: number; | |
| total_tokens: number; | |
| apis?: Record<string, APISnapshot>; | |
| requests_by_day?: Record<string, number>; | |
| requests_by_hour?: Record<string, number>; | |
| tokens_by_day?: Record<string, number>; | |
| tokens_by_hour?: Record<string, number>; | |
| } | |
| // Auth file types | |
| export interface AuthFile { | |
| name: string; | |
| provider: string; | |
| models?: string[]; | |
| enabled: boolean; | |
| created_at?: string; | |
| } | |
| // API Error class | |
| export class ApiError extends Error { | |
| status: number; | |
| detail: string; | |
| constructor(status: number, detail: string) { | |
| super(detail); | |
| this.status = status; | |
| this.detail = detail; | |
| this.name = 'ApiError'; | |
| } | |
| } | |
| // Connection state | |
| let isConnected = true; | |
| let connectionListeners: ((connected: boolean) => void)[] = []; | |
| export function onConnectionChange(listener: (connected: boolean) => void) { | |
| connectionListeners.push(listener); | |
| return () => { | |
| connectionListeners = connectionListeners.filter(l => l !== listener); | |
| }; | |
| } | |
| function setConnected(connected: boolean) { | |
| if (isConnected !== connected) { | |
| isConnected = connected; | |
| connectionListeners.forEach(l => l(connected)); | |
| } | |
| } | |
| export function getConnectionStatus() { | |
| return isConnected; | |
| } | |
| // Core fetch function with error handling | |
| async function fetchApi<T>(endpoint: string, options?: RequestInit): Promise<T> { | |
| try { | |
| const managementKey = getGlobalManagementKey(); | |
| const headers: Record<string, string> = { | |
| 'Content-Type': 'application/json', | |
| ...options?.headers as Record<string, string>, | |
| }; | |
| if (managementKey) { | |
| headers['Authorization'] = `Bearer ${managementKey}`; | |
| } | |
| const res = await fetch(`${API_BASE}${endpoint}`, { | |
| ...options, | |
| headers, | |
| }); | |
| setConnected(true); | |
| if (!res.ok) { | |
| const error = await res.json().catch(() => ({ detail: 'Request failed' })); | |
| throw new ApiError(res.status, error.error || error.detail || `HTTP ${res.status}`); | |
| } | |
| // Handle empty responses | |
| const text = await res.text(); | |
| if (!text) return {} as T; | |
| return JSON.parse(text); | |
| } catch (error) { | |
| if (error instanceof ApiError) { | |
| throw error; | |
| } | |
| // Network error - backend not reachable | |
| if (error instanceof TypeError && error.message.includes('fetch')) { | |
| setConnected(false); | |
| throw new ApiError(0, 'Cannot connect to backend server'); | |
| } | |
| setConnected(false); | |
| throw new ApiError(0, error instanceof Error ? error.message : 'Unknown error'); | |
| } | |
| } | |
| // ============ Health Check ============ | |
| export interface HealthStatus { | |
| status: string; | |
| version?: string; | |
| components?: { | |
| database?: boolean; | |
| providers?: boolean; | |
| }; | |
| } | |
| export async function checkHealth(): Promise<HealthStatus> { | |
| const res = await fetch('/api/health'); | |
| if (!res.ok) { | |
| throw new Error('Health check failed'); | |
| } | |
| setConnected(true); | |
| const data = await res.json(); | |
| return { | |
| status: data.status || 'healthy', | |
| version: data.version, | |
| components: data.components, | |
| }; | |
| } | |
| export async function checkReady(): Promise<{ ready: boolean }> { | |
| const res = await fetch('/api/ready'); | |
| if (!res.ok) { | |
| throw new Error('Ready check failed'); | |
| } | |
| return res.json(); | |
| } | |
| // ============ Usage Statistics ============ | |
| export async function getUsageStatistics(): Promise<{ usage: UsageSnapshot; failed_requests: number }> { | |
| return fetchApi('/usage'); | |
| } | |
| export async function exportUsageStatistics(): Promise<{ version: number; exported_at: string; usage: UsageSnapshot }> { | |
| return fetchApi('/usage/export'); | |
| } | |
| export async function importUsageStatistics(data: { version: number; usage: UsageSnapshot }): Promise<{ added: number; skipped: number; total_requests: number; failed_requests: number }> { | |
| return fetchApi('/usage/import', { | |
| method: 'POST', | |
| body: JSON.stringify(data), | |
| }); | |
| } | |
| // ============ Config ============ | |
| export async function getConfig(): Promise<Record<string, unknown>> { | |
| return fetchApi('/config'); | |
| } | |
| export async function getConfigYAML(): Promise<string> { | |
| const res = await fetch(`${API_BASE}/config.yaml`); | |
| return res.text(); | |
| } | |
| export async function putConfigYAML(yaml: string): Promise<{ status: string }> { | |
| return fetchApi('/config.yaml', { | |
| method: 'PUT', | |
| headers: { 'Content-Type': 'text/yaml' }, | |
| body: yaml, | |
| }); | |
| } | |
| // ============ API Keys (Access Control) ============ | |
| export async function getAPIKeys(): Promise<{ 'api-keys': string[] }> { | |
| return fetchApi('/api-keys'); | |
| } | |
| export async function putAPIKeys(keys: string[]): Promise<{ status: string }> { | |
| return fetchApi('/api-keys', { | |
| method: 'PUT', | |
| body: JSON.stringify(keys), | |
| }); | |
| } | |
| export async function patchAPIKeys(data: { old?: string; new?: string; index?: number; value?: string }): Promise<{ status: string }> { | |
| return fetchApi('/api-keys', { | |
| method: 'PATCH', | |
| body: JSON.stringify(data), | |
| }); | |
| } | |
| export async function deleteAPIKey(params: { index?: number; value?: string }): Promise<{ status: string }> { | |
| const query = new URLSearchParams(); | |
| if (params.index !== undefined) query.set('index', params.index.toString()); | |
| if (params.value) query.set('value', params.value); | |
| return fetchApi(`/api-keys?${query}`, { method: 'DELETE' }); | |
| } | |
| // ============ Live Models API (/v1/models) ============ | |
| export interface V1Model { | |
| id: string; | |
| object: string; | |
| created?: number; | |
| owned_by?: string; | |
| } | |
| export interface V1ModelsResponse { | |
| object: string; | |
| data: V1Model[]; | |
| } | |
| /** | |
| * Fetch models from /v1/models endpoint using an API key for authentication. | |
| * This calls the OpenAI-compatible models endpoint directly (not the management API). | |
| */ | |
| export async function fetchV1Models(apiKey: string): Promise<V1ModelsResponse> { | |
| const res = await fetch('/v1/models', { | |
| headers: { | |
| 'Authorization': `Bearer ${apiKey}`, | |
| }, | |
| }); | |
| if (!res.ok) { | |
| const error = await res.json().catch(() => ({ error: { message: 'Request failed' } })); | |
| throw new ApiError(res.status, error.error?.message || error.detail || `HTTP ${res.status}`); | |
| } | |
| return res.json(); | |
| } | |
| // ============ Gemini API Keys ============ | |
| export async function getGeminiKeys(): Promise<{ 'gemini-api-key': GeminiKey[] }> { | |
| return fetchApi('/gemini-api-key'); | |
| } | |
| export async function putGeminiKeys(keys: GeminiKey[]): Promise<{ status: string }> { | |
| return fetchApi('/gemini-api-key', { | |
| method: 'PUT', | |
| body: JSON.stringify(keys), | |
| }); | |
| } | |
| export async function patchGeminiKey(data: { index?: number; match?: string; value: Partial<GeminiKey> }): Promise<{ status: string }> { | |
| return fetchApi('/gemini-api-key', { | |
| method: 'PATCH', | |
| body: JSON.stringify(data), | |
| }); | |
| } | |
| export async function deleteGeminiKey(params: { 'api-key'?: string; index?: number }): Promise<{ status: string }> { | |
| const query = new URLSearchParams(); | |
| if (params['api-key']) query.set('api-key', params['api-key']); | |
| if (params.index !== undefined) query.set('index', params.index.toString()); | |
| return fetchApi(`/gemini-api-key?${query}`, { method: 'DELETE' }); | |
| } | |
| // ============ Claude API Keys ============ | |
| export async function getClaudeKeys(): Promise<{ 'claude-api-key': ClaudeKey[] }> { | |
| return fetchApi('/claude-api-key'); | |
| } | |
| export async function putClaudeKeys(keys: ClaudeKey[]): Promise<{ status: string }> { | |
| return fetchApi('/claude-api-key', { | |
| method: 'PUT', | |
| body: JSON.stringify(keys), | |
| }); | |
| } | |
| export async function patchClaudeKey(data: { index?: number; match?: string; value: Partial<ClaudeKey> }): Promise<{ status: string }> { | |
| return fetchApi('/claude-api-key', { | |
| method: 'PATCH', | |
| body: JSON.stringify(data), | |
| }); | |
| } | |
| export async function deleteClaudeKey(params: { 'api-key'?: string; index?: number }): Promise<{ status: string }> { | |
| const query = new URLSearchParams(); | |
| if (params['api-key']) query.set('api-key', params['api-key']); | |
| if (params.index !== undefined) query.set('index', params.index.toString()); | |
| return fetchApi(`/claude-api-key?${query}`, { method: 'DELETE' }); | |
| } | |
| // ============ Codex API Keys ============ | |
| export async function getCodexKeys(): Promise<{ 'codex-api-key': CodexKey[] }> { | |
| return fetchApi('/codex-api-key'); | |
| } | |
| export async function putCodexKeys(keys: CodexKey[]): Promise<{ status: string }> { | |
| return fetchApi('/codex-api-key', { | |
| method: 'PUT', | |
| body: JSON.stringify(keys), | |
| }); | |
| } | |
| export async function patchCodexKey(data: { index?: number; match?: string; value: Partial<CodexKey> }): Promise<{ status: string }> { | |
| return fetchApi('/codex-api-key', { | |
| method: 'PATCH', | |
| body: JSON.stringify(data), | |
| }); | |
| } | |
| export async function deleteCodexKey(params: { 'api-key'?: string; index?: number }): Promise<{ status: string }> { | |
| const query = new URLSearchParams(); | |
| if (params['api-key']) query.set('api-key', params['api-key']); | |
| if (params.index !== undefined) query.set('index', params.index.toString()); | |
| return fetchApi(`/codex-api-key?${query}`, { method: 'DELETE' }); | |
| } | |
| // ============ Vertex API Keys ============ | |
| export async function getVertexKeys(): Promise<{ 'vertex-api-key': VertexCompatKey[] }> { | |
| return fetchApi('/vertex-api-key'); | |
| } | |
| export async function putVertexKeys(keys: VertexCompatKey[]): Promise<{ status: string }> { | |
| return fetchApi('/vertex-api-key', { | |
| method: 'PUT', | |
| body: JSON.stringify(keys), | |
| }); | |
| } | |
| export async function patchVertexKey(data: { index?: number; match?: string; value: Partial<VertexCompatKey> }): Promise<{ status: string }> { | |
| return fetchApi('/vertex-api-key', { | |
| method: 'PATCH', | |
| body: JSON.stringify(data), | |
| }); | |
| } | |
| export async function deleteVertexKey(params: { 'api-key'?: string; index?: number }): Promise<{ status: string }> { | |
| const query = new URLSearchParams(); | |
| if (params['api-key']) query.set('api-key', params['api-key']); | |
| if (params.index !== undefined) query.set('index', params.index.toString()); | |
| return fetchApi(`/vertex-api-key?${query}`, { method: 'DELETE' }); | |
| } | |
| // ============ OpenAI Compatibility ============ | |
| export async function getOpenAICompat(): Promise<{ 'openai-compatibility': OpenAICompatibility[] }> { | |
| return fetchApi('/openai-compatibility'); | |
| } | |
| export async function putOpenAICompat(entries: OpenAICompatibility[]): Promise<{ status: string }> { | |
| return fetchApi('/openai-compatibility', { | |
| method: 'PUT', | |
| body: JSON.stringify(entries), | |
| }); | |
| } | |
| export async function patchOpenAICompat(data: { name?: string; index?: number; value: Partial<OpenAICompatibility> }): Promise<{ status: string }> { | |
| return fetchApi('/openai-compatibility', { | |
| method: 'PATCH', | |
| body: JSON.stringify(data), | |
| }); | |
| } | |
| export async function deleteOpenAICompat(params: { name?: string; index?: number }): Promise<{ status: string }> { | |
| const query = new URLSearchParams(); | |
| if (params.name) query.set('name', params.name); | |
| if (params.index !== undefined) query.set('index', params.index.toString()); | |
| return fetchApi(`/openai-compatibility?${query}`, { method: 'DELETE' }); | |
| } | |
| // ============ OAuth Model Alias ============ | |
| export async function getOAuthModelAlias(): Promise<{ 'oauth-model-alias': Record<string, OAuthModelAlias[]> }> { | |
| return fetchApi('/oauth-model-alias'); | |
| } | |
| export async function putOAuthModelAlias(data: Record<string, OAuthModelAlias[]>): Promise<{ status: string }> { | |
| return fetchApi('/oauth-model-alias', { | |
| method: 'PUT', | |
| body: JSON.stringify(data), | |
| }); | |
| } | |
| export async function patchOAuthModelAlias(data: { provider?: string; channel?: string; aliases: OAuthModelAlias[] }): Promise<{ status: string }> { | |
| return fetchApi('/oauth-model-alias', { | |
| method: 'PATCH', | |
| body: JSON.stringify(data), | |
| }); | |
| } | |
| export async function deleteOAuthModelAlias(channel: string): Promise<{ status: string }> { | |
| return fetchApi(`/oauth-model-alias?channel=${encodeURIComponent(channel)}`, { method: 'DELETE' }); | |
| } | |
| // ============ OAuth Excluded Models ============ | |
| export async function getOAuthExcludedModels(): Promise<{ 'oauth-excluded-models': Record<string, string[]> }> { | |
| return fetchApi('/oauth-excluded-models'); | |
| } | |
| export async function putOAuthExcludedModels(data: Record<string, string[]>): Promise<{ status: string }> { | |
| return fetchApi('/oauth-excluded-models', { | |
| method: 'PUT', | |
| body: JSON.stringify(data), | |
| }); | |
| } | |
| export async function patchOAuthExcludedModels(data: { provider: string; models: string[] }): Promise<{ status: string }> { | |
| return fetchApi('/oauth-excluded-models', { | |
| method: 'PATCH', | |
| body: JSON.stringify(data), | |
| }); | |
| } | |
| export async function deleteOAuthExcludedModels(provider: string): Promise<{ status: string }> { | |
| return fetchApi(`/oauth-excluded-models?provider=${encodeURIComponent(provider)}`, { method: 'DELETE' }); | |
| } | |
| // ============ Auth Files ============ | |
| export async function listAuthFiles(): Promise<{ files: AuthFile[] }> { | |
| return fetchApi('/auth-files'); | |
| } | |
| export async function getAuthFileModels(name: string): Promise<{ models: string[] }> { | |
| return fetchApi(`/auth-files/models?name=${encodeURIComponent(name)}`); | |
| } | |
| export async function uploadAuthFile(file: File): Promise<{ status: string; name: string }> { | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| const res = await fetch(`${API_BASE}/auth-files`, { | |
| method: 'POST', | |
| body: formData, | |
| }); | |
| if (!res.ok) { | |
| const error = await res.json().catch(() => ({ error: 'Upload failed' })); | |
| throw new ApiError(res.status, error.error || 'Upload failed'); | |
| } | |
| return res.json(); | |
| } | |
| export async function deleteAuthFile(name: string): Promise<{ status: string }> { | |
| return fetchApi(`/auth-files?name=${encodeURIComponent(name)}`, { method: 'DELETE' }); | |
| } | |
| export async function patchAuthFileStatus(name: string, enabled: boolean): Promise<{ status: string }> { | |
| return fetchApi('/auth-files/status', { | |
| method: 'PATCH', | |
| body: JSON.stringify({ name, enabled }), | |
| }); | |
| } | |
| // ============ Debug & Logging ============ | |
| export async function getDebug(): Promise<{ debug: boolean }> { | |
| return fetchApi('/debug'); | |
| } | |
| export async function putDebug(value: boolean): Promise<{ status: string }> { | |
| return fetchApi('/debug', { | |
| method: 'PUT', | |
| body: JSON.stringify({ value }), | |
| }); | |
| } | |
| export async function getLoggingToFile(): Promise<{ 'logging-to-file': boolean }> { | |
| return fetchApi('/logging-to-file'); | |
| } | |
| export async function putLoggingToFile(value: boolean): Promise<{ status: string }> { | |
| return fetchApi('/logging-to-file', { | |
| method: 'PUT', | |
| body: JSON.stringify({ value }), | |
| }); | |
| } | |
| export async function getUsageStatisticsEnabled(): Promise<{ 'usage-statistics-enabled': boolean }> { | |
| return fetchApi('/usage-statistics-enabled'); | |
| } | |
| export async function putUsageStatisticsEnabled(value: boolean): Promise<{ status: string }> { | |
| return fetchApi('/usage-statistics-enabled', { | |
| method: 'PUT', | |
| body: JSON.stringify({ value }), | |
| }); | |
| } | |
| // ============ Logs ============ | |
| export interface LogLine { | |
| timestamp?: number; | |
| level?: string; | |
| message?: string; | |
| raw?: string; | |
| } | |
| export interface LogsResponse { | |
| lines?: LogLine[]; | |
| 'line-count'?: number; | |
| 'latest-timestamp'?: number; | |
| logs?: string; // Fallback for simple string response | |
| } | |
| export async function getLogs(after?: string, limit?: number): Promise<LogsResponse> { | |
| const params = new URLSearchParams(); | |
| if (after) params.append('after', after); | |
| if (limit) params.append('limit', limit.toString()); | |
| const query = params.toString() ? `?${params}` : ''; | |
| return fetchApi(`/logs${query}`); | |
| } | |
| export async function deleteLogs(): Promise<{ success: boolean; message: string; removed: number }> { | |
| return fetchApi('/logs', { method: 'DELETE' }); | |
| } | |
| export async function getRequestLog(): Promise<{ 'request-log': boolean }> { | |
| return fetchApi('/request-log'); | |
| } | |
| export async function putRequestLog(value: boolean): Promise<{ status: string }> { | |
| return fetchApi('/request-log', { | |
| method: 'PUT', | |
| body: JSON.stringify({ value }), | |
| }); | |
| } | |
| export async function getErrorLogs(): Promise<{ files: { name: string; size: number; modified: number }[] }> { | |
| return fetchApi('/request-error-logs'); | |
| } | |
| export async function downloadErrorLog(filename: string): Promise<string> { | |
| // Use custom fetch to get blob/text directly since backend might return file content | |
| const managementKey = getGlobalManagementKey(); | |
| const headers: Record<string, string> = {}; | |
| if (managementKey) { | |
| headers['Authorization'] = `Bearer ${managementKey}`; | |
| } | |
| const res = await fetch(`${API_BASE}/request-error-logs/${encodeURIComponent(filename)}`, { headers }); | |
| if (!res.ok) throw new Error('Download failed'); | |
| return res.text(); | |
| } | |
| // ============ Tools & Batch ============ | |
| export async function batchValidateKeys(keys: string[], provider: string): Promise<{ valid: string[]; invalid: string[] }> { | |
| return fetchApi('/batch/validate', { | |
| method: 'POST', | |
| body: JSON.stringify({ keys, provider }), | |
| }); | |
| } | |
| export async function batchAddKeys(keys: string[], provider: string): Promise<{ added: number; skipped: number }> { | |
| return fetchApi('/batch/add', { | |
| method: 'POST', | |
| body: JSON.stringify({ keys, provider }), | |
| }); | |
| } | |
| export async function createBackup(): Promise<{ id: string; url: string; created_at: string }> { | |
| return fetchApi('/backup/create', { method: 'POST' }); | |
| } | |
| export async function restoreBackup(file: File): Promise<{ status: string }> { | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| const managementKey = getGlobalManagementKey(); | |
| const headers: Record<string, string> = {}; | |
| if (managementKey) { | |
| headers['Authorization'] = `Bearer ${managementKey}`; | |
| } | |
| const res = await fetch(`${API_BASE}/backup/restore`, { | |
| method: 'POST', | |
| headers, | |
| body: formData, | |
| }); | |
| if (!res.ok) { | |
| const error = await res.json().catch(() => ({ detail: 'Restore failed' })); | |
| throw new ApiError(res.status, error.error || error.detail); | |
| } | |
| return res.json(); | |
| } | |
| export async function testProvider(provider: string): Promise<{ status: 'success' | 'failed'; latency: number; error?: string }> { | |
| return fetchApi(`/test-provider?provider=${encodeURIComponent(provider)}`); | |
| } | |
| // ============ OAuth & Auth Files ============ | |
| export async function submitCallback(url: string): Promise<{ status: string }> { | |
| return fetchApi('/oauth/callback', { | |
| method: 'POST', | |
| body: JSON.stringify({ url }), | |
| }); | |
| } | |
| export async function iflowCookieAuth(cookie: string): Promise<{ status: string }> { | |
| return fetchApi('/oauth/iflow-cookie', { | |
| method: 'POST', | |
| body: JSON.stringify({ cookie }), | |
| }); | |
| } | |
| export async function importVertexCredential(file: File, location?: string): Promise<{ status: string }> { | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| if (location) formData.append('location', location); | |
| const managementKey = getGlobalManagementKey(); | |
| const headers: Record<string, string> = {}; | |
| if (managementKey) { | |
| headers['Authorization'] = `Bearer ${managementKey}`; | |
| } | |
| const res = await fetch(`${API_BASE}/vertex-credential`, { | |
| method: 'POST', | |
| headers, | |
| body: formData, | |
| }); | |
| if (!res.ok) { | |
| const error = await res.json().catch(() => ({ detail: 'Import failed' })); | |
| throw new ApiError(res.status, error.error || error.detail); | |
| } | |
| return res.json(); | |
| } | |
| // ============ Routing ============ | |
| export async function getRoutingStrategy(): Promise<{ strategy: string }> { | |
| return fetchApi('/routing/strategy'); | |
| } | |
| export async function putRoutingStrategy(value: string): Promise<{ status: string }> { | |
| return fetchApi('/routing/strategy', { | |
| method: 'PUT', | |
| body: JSON.stringify({ value }), | |
| }); | |
| } | |
| // ============ Proxy URL ============ | |
| export async function getProxyURL(): Promise<{ 'proxy-url': string }> { | |
| return fetchApi('/proxy-url'); | |
| } | |
| export async function putProxyURL(value: string): Promise<{ status: string }> { | |
| return fetchApi('/proxy-url', { | |
| method: 'PUT', | |
| body: JSON.stringify({ value }), | |
| }); | |
| } | |
| export async function deleteProxyURL(): Promise<{ status: string }> { | |
| return fetchApi('/proxy-url', { method: 'DELETE' }); | |
| } | |
| // ============ Request Retry ============ | |
| export async function getRequestRetry(): Promise<{ 'request-retry': number }> { | |
| return fetchApi('/request-retry'); | |
| } | |
| export async function putRequestRetry(value: number): Promise<{ status: string }> { | |
| return fetchApi('/request-retry', { | |
| method: 'PUT', | |
| body: JSON.stringify({ value }), | |
| }); | |
| } | |
| // ============ OAuth Authentication URLs ============ | |
| export async function getAnthropicAuthURL(): Promise<{ url: string; state: string }> { | |
| return fetchApi('/anthropic-auth-url'); | |
| } | |
| export async function getCodexAuthURL(): Promise<{ url: string; state: string }> { | |
| return fetchApi('/codex-auth-url'); | |
| } | |
| export async function getGeminiCLIAuthURL(): Promise<{ url: string; state: string }> { | |
| return fetchApi('/gemini-cli-auth-url'); | |
| } | |
| export async function getAuthStatus(provider: string, state: string): Promise<{ status: string; error?: string }> { | |
| return fetchApi(`/get-auth-status?provider=${encodeURIComponent(provider)}&state=${encodeURIComponent(state)}`); | |
| } | |
| // ============ Version Info ============ | |
| export async function getLatestVersion(): Promise<{ version: string; current: string; update_available: boolean }> { | |
| return fetchApi('/latest-version'); | |
| } | |
| // ============ Utility functions ============ | |
| export function formatNumber(num: number): string { | |
| if (num === null || num === undefined) return '0'; | |
| if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'; | |
| if (num >= 1000) return (num / 1000).toFixed(1) + 'K'; | |
| return num.toString(); | |
| } | |
| export function formatBytes(bytes: number): string { | |
| if (bytes === 0) return '0 B'; | |
| const k = 1024; | |
| const sizes = ['B', 'KB', 'MB', 'GB']; | |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; | |
| } | |
| export function formatDate(dateStr: string): string { | |
| if (!dateStr) return '-'; | |
| const date = new Date(dateStr); | |
| return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); | |
| } | |
| export function formatDuration(seconds: number): string { | |
| if (seconds < 60) return `${seconds}s`; | |
| if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`; | |
| const hours = Math.floor(seconds / 3600); | |
| const mins = Math.floor((seconds % 3600) / 60); | |
| if (hours < 24) return `${hours}h ${mins}m`; | |
| const days = Math.floor(hours / 24); | |
| return `${days}d ${hours % 24}h`; | |
| } | |
| export function formatLatency(ms: number): string { | |
| if (ms < 1000) return `${Math.round(ms)}ms`; | |
| return `${(ms / 1000).toFixed(2)}s`; | |
| } | |
| export function formatPercentage(value: number, decimals: number = 1): string { | |
| return `${value.toFixed(decimals)}%`; | |
| } | |
| export function getStatusColor(status: string): string { | |
| switch (status) { | |
| case 'healthy': | |
| case 'success': | |
| return 'text-white'; | |
| case 'degraded': | |
| case 'warning': | |
| return 'text-white/70'; | |
| case 'unhealthy': | |
| case 'error': | |
| return 'text-white/50'; | |
| default: | |
| return 'text-muted-foreground'; | |
| } | |
| } | |
| export function getRelativeTime(dateStr: string): string { | |
| if (!dateStr) return 'Never'; | |
| const date = new Date(dateStr); | |
| const now = new Date(); | |
| const diff = now.getTime() - date.getTime(); | |
| const seconds = Math.floor(diff / 1000); | |
| const minutes = Math.floor(seconds / 60); | |
| const hours = Math.floor(minutes / 60); | |
| const days = Math.floor(hours / 24); | |
| if (seconds < 60) return 'Just now'; | |
| if (minutes < 60) return `${minutes}m ago`; | |
| if (hours < 24) return `${hours}h ago`; | |
| if (days < 7) return `${days}d ago`; | |
| return formatDate(dateStr); | |
| } | |
| export function maskApiKey(key: string): string { | |
| if (!key || key.length < 8) return '****'; | |
| return key.substring(0, 4) + '****' + key.substring(key.length - 4); | |
| } | |
| // ============ Extended Auth Files ============ | |
| export interface AuthFileItem { | |
| name: string; | |
| type?: string; | |
| provider?: string; | |
| size?: number; | |
| modtime?: number; | |
| modified?: string; | |
| disabled?: boolean; | |
| auth_index?: number | string; | |
| authIndex?: number | string; | |
| runtime_only?: boolean; | |
| runtimeOnly?: boolean; | |
| } | |
| export async function getAuthFiles(): Promise<{ files: AuthFileItem[] }> { | |
| return fetchApi('/auth-files'); | |
| } | |
| export async function deleteAllAuthFiles(): Promise<{ status: string }> { | |
| return fetchApi('/auth-files?all=true', { method: 'DELETE' }); | |
| } | |
| export async function downloadAuthFile(name: string): Promise<string> { | |
| const managementKey = getGlobalManagementKey(); | |
| const headers: Record<string, string> = {}; | |
| if (managementKey) { | |
| headers['Authorization'] = `Bearer ${managementKey}`; | |
| } | |
| const res = await fetch(`${API_BASE}/auth-files/download?name=${encodeURIComponent(name)}`, { headers }); | |
| if (!res.ok) throw new Error('Download failed'); | |
| return res.text(); | |
| } | |
| export async function setAuthFileStatus(name: string, disabled: boolean): Promise<{ disabled: boolean }> { | |
| return fetchApi('/auth-files/status', { | |
| method: 'PATCH', | |
| body: JSON.stringify({ name, disabled }), | |
| }); | |
| } | |
| // ============ OAuth Providers ============ | |
| export type OAuthProvider = 'codex' | 'anthropic' | 'antigravity' | 'gemini-cli' | 'qwen' | 'iflow'; | |
| export interface OAuthStartResponse { | |
| status: string; | |
| url: string; | |
| state: string; | |
| } | |
| export interface OAuthStatusResponse { | |
| status: 'ok' | 'wait' | 'error'; | |
| error?: string; | |
| } | |
| // Get the correct auth URL endpoint for each provider | |
| function getAuthUrlEndpoint(provider: OAuthProvider): string { | |
| switch (provider) { | |
| case 'codex': return '/codex-auth-url'; | |
| case 'anthropic': return '/anthropic-auth-url'; | |
| case 'antigravity': return '/antigravity-auth-url'; | |
| case 'gemini-cli': return '/gemini-cli-auth-url'; | |
| case 'qwen': return '/qwen-auth-url'; | |
| case 'iflow': return '/iflow-auth-url'; | |
| default: return `/${provider}-auth-url`; | |
| } | |
| } | |
| export async function startOAuth(provider: OAuthProvider, options?: { projectId?: string }): Promise<OAuthStartResponse> { | |
| const endpoint = getAuthUrlEndpoint(provider); | |
| const params = new URLSearchParams(); | |
| params.append('is_webui', '1'); | |
| if (options?.projectId && provider === 'gemini-cli') { | |
| params.append('project_id', options.projectId); | |
| } | |
| return fetchApi(`${endpoint}?${params}`); | |
| } | |
| export async function getOAuthStatus(state: string): Promise<OAuthStatusResponse> { | |
| return fetchApi(`/get-auth-status?state=${encodeURIComponent(state)}`); | |
| } | |
| export async function submitOAuthCallback(provider: OAuthProvider, redirectUrl: string): Promise<{ status: string }> { | |
| // Parse the redirect URL to extract code and state | |
| try { | |
| const url = new URL(redirectUrl); | |
| const code = url.searchParams.get('code') || ''; | |
| const state = url.searchParams.get('state') || ''; | |
| const error = url.searchParams.get('error') || ''; | |
| return fetchApi('/oauth-callback', { | |
| method: 'POST', | |
| body: JSON.stringify({ provider, code, state, error, redirect_url: redirectUrl }), | |
| }); | |
| } catch { | |
| return fetchApi('/oauth-callback', { | |
| method: 'POST', | |
| body: JSON.stringify({ provider, redirect_url: redirectUrl }), | |
| }); | |
| } | |
| } | |
| export interface IFlowCookieAuthResponse { | |
| status: 'ok' | 'error'; | |
| email?: string; | |
| expired?: string; | |
| saved_path?: string; | |
| type?: string; | |
| error?: string; | |
| } | |
| export async function iflowCookieAuthFull(cookie: string): Promise<IFlowCookieAuthResponse> { | |
| return fetchApi('/iflow-auth-url', { | |
| method: 'POST', | |
| body: JSON.stringify({ cookie }), | |
| }); | |
| } | |
| export interface VertexImportResponse { | |
| project_id?: string; | |
| email?: string; | |
| location?: string; | |
| 'auth-file'?: string; | |
| auth_file?: string; | |
| } | |
| export async function importVertexCredentialFull(file: File, location?: string): Promise<VertexImportResponse> { | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| if (location) formData.append('location', location); | |
| const managementKey = getGlobalManagementKey(); | |
| const headers: Record<string, string> = {}; | |
| if (managementKey) { | |
| headers['Authorization'] = `Bearer ${managementKey}`; | |
| } | |
| const res = await fetch(`${API_BASE}/vertex/import`, { | |
| method: 'POST', | |
| headers, | |
| body: formData, | |
| }); | |
| if (!res.ok) { | |
| const error = await res.json().catch(() => ({ detail: 'Import failed' })); | |
| throw new ApiError(res.status, error.error || error.detail); | |
| } | |
| return res.json(); | |
| } | |
| // ============ OAuth Model Mappings (for Auth Files) ============ | |
| export interface OAuthModelMappingEntry { | |
| name: string; | |
| alias: string; | |
| fork?: boolean; | |
| } | |
| export async function getOAuthModelMappings(): Promise<Record<string, OAuthModelMappingEntry[]>> { | |
| return fetchApi('/oauth-model-mappings'); | |
| } | |
| export async function saveOAuthModelMappings(provider: string, mappings: OAuthModelMappingEntry[]): Promise<{ status: string }> { | |
| return fetchApi('/oauth-model-mappings', { | |
| method: 'PATCH', | |
| body: JSON.stringify({ provider, mappings }), | |
| }); | |
| } | |
| export async function deleteOAuthModelMappings(provider: string): Promise<{ status: string }> { | |
| return fetchApi(`/oauth-model-mappings?provider=${encodeURIComponent(provider)}`, { method: 'DELETE' }); | |
| } | |