/** * API Client * Phase 03: REST API Routes & CRUD */ const BASE_URL = '/api'; async function request(url: string, options?: RequestInit): Promise { const res = await fetch(`${BASE_URL}${url}`, { headers: { 'Content-Type': 'application/json' }, ...options, }); if (!res.ok) { const error = await res.json().catch(() => ({ error: 'Unknown error' })); throw new Error(error.error || res.statusText); } return res.json(); } // Types export interface Profile { name: string; settingsPath: string; configured: boolean; } export interface CreateProfile { name: string; baseUrl: string; apiKey: string; model?: string; opusModel?: string; sonnetModel?: string; haikuModel?: string; } export interface UpdateProfile { baseUrl?: string; apiKey?: string; model?: string; opusModel?: string; sonnetModel?: string; haikuModel?: string; } export interface Variant { name: string; provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude'; settings: string; account?: string; port?: number; model?: string; } export interface CreateVariant { name: string; provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude'; model?: string; account?: string; } export interface UpdateVariant { provider?: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude'; model?: string; account?: string; } /** OAuth account info for multi-account support */ export interface OAuthAccount { id: string; email?: string; nickname?: string; provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude'; isDefault: boolean; tokenFile: string; createdAt: string; lastUsedAt?: string; /** Whether account is paused (skipped in quota rotation) */ paused?: boolean; /** ISO timestamp when account was paused */ pausedAt?: string; /** Account tier: free or paid (Pro/Ultra combined) */ tier?: 'free' | 'paid' | 'unknown'; /** GCP Project ID (Antigravity only) - read-only */ projectId?: string; } export interface AuthStatus { provider: string; displayName: string; authenticated: boolean; lastAuth: string | null; tokenFiles: number; accounts: OAuthAccount[]; defaultAccount?: string; } /** Auth file info for Config tab */ export interface AuthFile { name: string; provider?: string; } /** CLIProxy model from /v1/models endpoint */ export interface CliproxyModel { id: string; object: string; created: number; owned_by: string; } /** Categorized models response from CLIProxyAPI */ export interface CliproxyModelsResponse { models: CliproxyModel[]; byCategory: Record; totalCount: number; } /** Individual model quota info from Google Cloud Code API */ export interface ModelQuota { /** Model name, e.g., "gemini-3-pro-high" */ name: string; /** Display name from API, e.g., "Gemini 3 Pro" */ displayName?: string; /** Remaining quota as percentage (0-100) */ percentage: number; /** ISO timestamp when quota resets, null if unknown */ resetTime: string | null; } /** Quota fetch result */ export interface QuotaResult { /** Whether fetch succeeded */ success: boolean; /** Quota for each available model */ models: ModelQuota[]; /** Timestamp of fetch */ lastUpdated: number; /** True if account lacks quota access (403) */ isForbidden?: boolean; /** Error message if fetch failed */ error?: string; } /** Provider accounts summary */ export type ProviderAccountsMap = Record; export interface Account { name: string; type?: string; created: string; last_used?: string | null; } // Unified config types export interface ConfigFormat { format: 'yaml' | 'json' | 'none'; migrationNeeded: boolean; backups: string[]; } export interface MigrationResult { success: boolean; backupPath?: string; error?: string; migratedFiles: string[]; warnings: string[]; } /** Model preset for quick model switching */ export interface ModelPreset { name: string; default: string; opus: string; sonnet: string; haiku: string; } export interface CreatePreset { name: string; default: string; opus?: string; sonnet?: string; haiku?: string; } /** Remote proxy status from health check */ export interface RemoteProxyStatus { reachable: boolean; latencyMs?: number; error?: string; errorCode?: 'CONNECTION_REFUSED' | 'TIMEOUT' | 'AUTH_FAILED' | 'UNKNOWN'; } /** Remote proxy configuration */ export interface ProxyRemoteConfig { enabled: boolean; host: string; /** Port is optional - uses protocol default (443 for HTTPS, 80 for HTTP) */ port?: number; protocol: 'http' | 'https'; auth_token: string; /** Management key for /v0/management/* endpoints (optional, falls back to auth_token) */ management_key?: string; } /** Fallback configuration */ export interface ProxyFallbackConfig { enabled: boolean; auto_start: boolean; } /** Local proxy configuration */ export interface ProxyLocalConfig { port: number; auto_start: boolean; } /** CLIProxy server configuration */ export interface CliproxyServerConfig { remote: ProxyRemoteConfig; fallback: ProxyFallbackConfig; local: ProxyLocalConfig; } /** CLIProxy process status from session tracker */ export interface ProxyProcessStatus { running: boolean; port?: number; pid?: number; sessionCount?: number; startedAt?: string; } /** Error log file metadata from CLIProxyAPI */ export interface CliproxyErrorLog { /** Filename (e.g., "error-v1-chat-completions-2025-01-15T10-30-00.log") */ name: string; /** File size in bytes */ size: number; /** Last modified timestamp (Unix seconds) */ modified: number; } /** Result from starting proxy service */ export interface ProxyStartResult { started: boolean; alreadyRunning: boolean; port: number; configRegenerated?: boolean; error?: string; } /** Result from stopping proxy service */ export interface ProxyStopResult { stopped: boolean; pid?: number; sessionCount?: number; error?: string; } /** Result from checking for CLIProxyAPI updates */ export interface CliproxyUpdateCheckResult { hasUpdate: boolean; currentVersion: string; latestVersion: string; fromCache: boolean; checkedAt: number; // Unix timestamp of last check // Backend info backend: 'original' | 'plus'; backendLabel: string; // Stability fields isStable: boolean; // Whether current version is at or below max stable maxStableVersion: string; // Maximum stable version (e.g., "6.6.80") stabilityMessage?: string; // Warning message if running unstable version } /** Available versions list from GitHub releases */ export interface CliproxyVersionsResponse { versions: string[]; latestStable: string; latest: string; currentVersion: string; maxStableVersion: string; fromCache: boolean; checkedAt: number; } /** Result from installing a specific version */ export interface CliproxyInstallResult { success: boolean; version?: string; isUnstable?: boolean; requiresConfirmation?: boolean; message?: string; error?: string; } /** Result from restarting the proxy */ export interface CliproxyRestartResult { success: boolean; port?: number; error?: string; } // API export const api = { profiles: { list: () => request<{ profiles: Profile[] }>('/profiles'), create: (data: CreateProfile) => request('/profiles', { method: 'POST', body: JSON.stringify(data), }), update: (name: string, data: UpdateProfile) => request(`/profiles/${name}`, { method: 'PUT', body: JSON.stringify(data), }), delete: (name: string) => request(`/profiles/${name}`, { method: 'DELETE' }), }, cliproxy: { list: () => request<{ variants: Variant[] }>('/cliproxy'), getAuthStatus: () => request<{ authStatus: AuthStatus[]; source?: 'remote' | 'local'; error?: string }>( '/cliproxy/auth' ), create: (data: CreateVariant) => request('/cliproxy', { method: 'POST', body: JSON.stringify(data), }), update: (name: string, data: UpdateVariant) => request(`/cliproxy/${name}`, { method: 'PUT', body: JSON.stringify(data), }), delete: (name: string) => request(`/cliproxy/${name}`, { method: 'DELETE' }), // Proxy process status and control proxyStatus: () => request('/cliproxy/proxy-status'), proxyStart: () => request('/cliproxy/proxy-start', { method: 'POST' }), proxyStop: () => request('/cliproxy/proxy-stop', { method: 'POST' }), updateCheck: () => request('/cliproxy/update-check'), // Version management versions: () => request('/cliproxy/versions'), install: (version: string, force?: boolean) => request('/cliproxy/install', { method: 'POST', body: JSON.stringify({ version, force }), }), restart: () => request('/cliproxy/restart', { method: 'POST' }), // Stats and models for Overview tab stats: () => request<{ usage: Record }>('/cliproxy/usage'), models: () => request('/cliproxy/models'), updateModel: (provider: string, model: string) => request(`/cliproxy/models/${provider}`, { method: 'PUT', body: JSON.stringify({ model }), }), // Config YAML for Config tab getConfigYaml: async (): Promise => { const res = await fetch(`${BASE_URL}/cliproxy/config.yaml`); if (!res.ok) throw new Error('Failed to load config'); return res.text(); }, saveConfigYaml: async (content: string): Promise => { const res = await fetch(`${BASE_URL}/cliproxy/config.yaml`, { method: 'PUT', headers: { 'Content-Type': 'application/yaml' }, body: content, }); if (!res.ok) { const error = await res.json().catch(() => ({ error: 'Failed to save config' })); throw new Error(error.error || 'Failed to save config'); } }, // Auth files for Config tab getAuthFiles: () => request<{ files: AuthFile[] }>('/cliproxy/auth-files'), getAuthFile: async (name: string): Promise => { const res = await fetch( `${BASE_URL}/cliproxy/auth-files/download?name=${encodeURIComponent(name)}` ); if (!res.ok) throw new Error('Failed to load auth file'); return res.text(); }, // Multi-account management accounts: { list: () => request<{ accounts: ProviderAccountsMap }>('/cliproxy/auth/accounts'), listByProvider: (provider: string) => request<{ provider: string; accounts: OAuthAccount[] }>( `/cliproxy/auth/accounts/${provider}` ), setDefault: (provider: string, accountId: string) => request(`/cliproxy/auth/accounts/${provider}/default`, { method: 'POST', body: JSON.stringify({ accountId }), }), remove: (provider: string, accountId: string) => request(`/cliproxy/auth/accounts/${provider}/${accountId}`, { method: 'DELETE' }), pause: (provider: string, accountId: string) => request<{ provider: string; accountId: string; paused: boolean }>( `/cliproxy/auth/accounts/${provider}/${accountId}/pause`, { method: 'POST' } ), resume: (provider: string, accountId: string) => request<{ provider: string; accountId: string; paused: boolean }>( `/cliproxy/auth/accounts/${provider}/${accountId}/resume`, { method: 'POST' } ), /** Solo mode: activate one account, pause all others */ solo: (provider: string, accountId: string) => request<{ activated: string; paused: string[] }>('/accounts/solo', { method: 'POST', body: JSON.stringify({ provider, accountId }), }), /** Bulk pause multiple accounts */ bulkPause: (provider: string, accountIds: string[]) => request<{ succeeded: string[]; failed: Array<{ id: string; reason: string }> }>( '/accounts/bulk-pause', { method: 'POST', body: JSON.stringify({ provider, accountIds }), } ), /** Bulk resume multiple accounts */ bulkResume: (provider: string, accountIds: string[]) => request<{ succeeded: string[]; failed: Array<{ id: string; reason: string }> }>( '/accounts/bulk-resume', { method: 'POST', body: JSON.stringify({ provider, accountIds }), } ), }, // OAuth flow auth: { /** Start OAuth flow - opens browser for authentication */ start: (provider: string, nickname?: string) => request<{ success: boolean; account: OAuthAccount }>(`/cliproxy/auth/${provider}/start`, { method: 'POST', body: JSON.stringify({ nickname }), }), /** Cancel in-progress OAuth flow */ cancel: (provider: string) => request<{ success: boolean; cancelled: number; provider: string }>( `/cliproxy/auth/${provider}/cancel`, { method: 'POST' } ), /** Import Kiro token from Kiro IDE (Kiro only) */ kiroImport: () => request<{ success: boolean; account: OAuthAccount | null; error?: string }>( '/cliproxy/auth/kiro/import', { method: 'POST' } ), }, // Error logs errorLogs: { /** List error log files */ list: () => request<{ files: CliproxyErrorLog[] }>('/cliproxy/error-logs'), /** Get content of a specific error log */ getContent: async (name: string): Promise => { const res = await fetch(`${BASE_URL}/cliproxy/error-logs/${encodeURIComponent(name)}`); if (!res.ok) throw new Error('Failed to load error log'); return res.text(); }, }, }, accounts: { list: () => request<{ accounts: Account[]; default: string | null }>('/accounts'), setDefault: (name: string) => request('/accounts/default', { method: 'POST', body: JSON.stringify({ name }), }), resetDefault: () => request('/accounts/reset-default', { method: 'DELETE' }), delete: (name: string) => request(`/accounts/${name}`, { method: 'DELETE' }), }, // Unified config API config: { format: () => request('/config/format'), get: () => request>('/config'), update: (config: Record) => request<{ success: boolean }>('/config', { method: 'PUT', body: JSON.stringify(config), }), migrate: (dryRun = false) => request(`/config/migrate?dryRun=${dryRun}`, { method: 'POST' }), rollback: (backupPath: string) => request<{ success: boolean }>('/config/rollback', { method: 'POST', body: JSON.stringify({ backupPath }), }), }, /** Model presets for quick model switching */ presets: { list: (profile: string) => request<{ presets: ModelPreset[] }>(`/settings/${profile}/presets`), create: (profile: string, data: CreatePreset) => request<{ preset: ModelPreset }>(`/settings/${profile}/presets`, { method: 'POST', body: JSON.stringify(data), }), delete: (profile: string, name: string) => request<{ success: boolean }>(`/settings/${profile}/presets/${encodeURIComponent(name)}`, { method: 'DELETE', }), }, /** CLIProxy server configuration API */ cliproxyServer: { /** Get cliproxy server configuration */ get: () => request('/cliproxy-server'), /** Update cliproxy server configuration */ update: (config: Partial) => request('/cliproxy-server', { method: 'PUT', body: JSON.stringify(config), }), /** Get backend setting */ getBackend: () => request<{ backend: 'original' | 'plus' }>('/cliproxy-server/backend'), /** Update backend setting */ updateBackend: (backend: 'original' | 'plus', force = false) => request<{ backend: 'original' | 'plus' }>('/cliproxy-server/backend', { method: 'PUT', body: JSON.stringify({ backend, force }), }), /** Test remote proxy connection */ test: (params: { host: string; /** Port is optional - uses protocol default (443 for HTTPS, 80 for HTTP) */ port?: number; protocol: 'http' | 'https'; authToken?: string; allowSelfSigned?: boolean; }) => request('/cliproxy-server/test', { method: 'POST', body: JSON.stringify(params), }), }, /** Account quota API */ quota: { /** Fetch quota for a specific account */ get: (provider: string, accountId: string) => request(`/cliproxy/quota/${provider}/${encodeURIComponent(accountId)}`), }, };