/** * API Client * Phase 03: REST API Routes & CRUD */ import type { CLIProxyProvider } from './provider-config'; import type { AiProviderFamilyId, ListAiProvidersResult, UpsertAiProviderEntryInput, } from '../../../src/cliproxy/ai-providers'; import type { ProviderEntitlementEvidence } from '../../../src/cliproxy/provider-entitlement-types'; import type { BrowserRuntimeEnv } from '../../../src/utils/browser/chrome-reuse'; export const API_BASE_URL = '/api'; export const API_CONFLICT_ERROR_CODE = 'CONFLICT'; export type { ProviderEntitlementEvidence }; export class ApiConflictError extends Error { readonly code = API_CONFLICT_ERROR_CODE; constructor(message = 'Resource modified externally') { super(message); this.name = 'ApiConflictError'; } } export function isApiConflictError(error: unknown): error is Error & { code: string } { return ( error instanceof Error && 'code' in error && (error as { code?: unknown }).code === API_CONFLICT_ERROR_CODE ); } export function withApiBase(path: string): string { if (!path) { return API_BASE_URL; } if (/^https?:\/\//i.test(path)) { return path; } if (path === API_BASE_URL || path.startsWith(`${API_BASE_URL}/`)) { return path; } return `${API_BASE_URL}${path.startsWith('/') ? path : `/${path}`}`; } async function parseErrorMessage(response: Response): Promise { const fallbackMessage = `Request failed (${response.status}${response.statusText ? ` ${response.statusText}` : ''})`; const bodyText = await response.text(); if (!bodyText) { return fallbackMessage; } try { const parsed = JSON.parse(bodyText) as { error?: string; message?: string }; if (parsed.error?.trim()) { return parsed.error; } if (parsed.message?.trim()) { return parsed.message; } return fallbackMessage; } catch { return bodyText.trim() || fallbackMessage; } } async function request(url: string, options?: RequestInit): Promise { const res = await fetch(withApiBase(url), { headers: { 'Content-Type': 'application/json' }, ...options, }); if (!res.ok) { throw new Error(await parseErrorMessage(res)); } if (res.status === 204) { return undefined as T; } const contentType = res.headers.get('content-type')?.toLowerCase() ?? ''; if (contentType.includes('application/json')) { return (await res.json()) as T; } const bodyText = await res.text(); if (!bodyText) { return undefined as T; } try { return JSON.parse(bodyText) as T; } catch { return bodyText as T; } } // Types export type CliTarget = 'claude' | 'droid' | 'codex'; export interface CliproxyBridgeMetadata { provider: CLIProxyProvider; providerDisplayName: string; routePath: string; currentBaseUrl: string; source: 'local' | 'remote'; usesCurrentTarget: boolean; usesCurrentAuthToken: boolean; } export interface ImageAnalysisStatus { enabled: boolean; supported: boolean; status: 'active' | 'mapped' | 'attention' | 'disabled' | 'skipped' | 'hook-missing'; backendId: string | null; backendDisplayName: string | null; model: string | null; resolutionSource: | 'cliproxy-provider' | 'cliproxy-variant' | 'cliproxy-composite' | 'copilot-alias' | 'cliproxy-bridge' | 'profile-backend' | 'fallback-backend' | 'native-compatible' | 'disabled' | 'unsupported-profile' | 'unresolved' | 'missing-model'; reason: string | null; shouldPersistHook: boolean; persistencePath: string | null; runtimePath: string | null; usesCurrentTarget: boolean | null; usesCurrentAuthToken: boolean | null; hookInstalled: boolean | null; sharedHookInstalled: boolean | null; authReadiness: 'not-needed' | 'ready' | 'missing' | 'unknown'; authProvider: string | null; authDisplayName: string | null; authReason: string | null; proxyReadiness: 'not-needed' | 'ready' | 'remote' | 'stopped' | 'unavailable' | 'unknown'; proxyReason: string | null; effectiveRuntimeMode: 'cliproxy-image-analysis' | 'native-read'; effectiveRuntimeReason: string | null; profileModel: string | null; nativeReadPreference: boolean; nativeImageCapable: boolean | null; nativeImageReason: string | null; } export interface ImageAnalysisSettingsConfig { enabled: boolean; timeout: number; providerModels: Record; fallbackBackend: string | null; profileBackends: Record; } export interface ImageAnalysisDashboardSummary { state: 'ready' | 'partial' | 'needs_setup' | 'disabled'; title: string; detail: string; backendCount: number; mappedProfileCount: number; activeProfileCount: number; bypassedProfileCount: number; nativeProfileCount: number; } export interface ImageAnalysisDashboardBackend { backendId: string; displayName: string; model: string; state: 'ready' | 'starts_on_launch' | 'needs_auth' | 'needs_proxy' | 'review'; authReadiness: ImageAnalysisStatus['authReadiness']; authReason: string | null; proxyReadiness: ImageAnalysisStatus['proxyReadiness']; proxyReason: string | null; profilesUsing: number; } export interface ImageAnalysisDashboardProfile { name: string; kind: 'profile' | 'variant'; target: CliTarget; configured: boolean; settingsPath: string | null; backendId: string | null; backendDisplayName: string | null; resolutionSource: ImageAnalysisStatus['resolutionSource']; status: ImageAnalysisStatus['status']; effectiveRuntimeMode: ImageAnalysisStatus['effectiveRuntimeMode']; effectiveRuntimeReason: string | null; currentTargetMode: | 'active' | 'bypassed' | 'fallback' | 'setup' | 'disabled' | 'native' | 'unresolved'; profileModel: string | null; nativeReadPreference: boolean; nativeImageCapable: boolean | null; nativeImageReason: string | null; } export interface ImageAnalysisDashboardCatalog { knownBackends: string[]; profileNames: string[]; } export interface ImageAnalysisDashboardData { config: ImageAnalysisSettingsConfig; summary: ImageAnalysisDashboardSummary; backends: ImageAnalysisDashboardBackend[]; profiles: ImageAnalysisDashboardProfile[]; catalog: ImageAnalysisDashboardCatalog; } export interface UpdateImageAnalysisSettingsPayload { enabled?: boolean; timeout?: number; providerModels?: Record; fallbackBackend?: string | null; profileBackends?: Record; } export interface BrowserSettingsConfig { claude: { enabled: boolean; userDataDir: string; devtoolsPort: number; }; codex: { enabled: boolean; }; } export interface BrowserLaunchCommands { darwin: string; linux: string; win32: string; } export interface ClaudeBrowserStatus { enabled: boolean; source: 'config' | 'CCS_BROWSER_USER_DATA_DIR' | 'CCS_BROWSER_PROFILE_DIR'; overrideActive: boolean; state: 'disabled' | 'path_missing' | 'browser_not_running' | 'endpoint_unreachable' | 'ready'; title: string; detail: string; nextStep: string; effectiveUserDataDir: string; recommendedUserDataDir: string; devtoolsPort: number; managedMcpServerName: string; managedMcpServerPath: string; launchCommands: BrowserLaunchCommands; runtimeEnv?: BrowserRuntimeEnv; } export interface CodexBrowserStatus { enabled: boolean; state: 'disabled' | 'enabled' | 'unsupported_build'; title: string; detail: string; nextStep: string; serverName: string; supportsConfigOverrides: boolean; binaryPath: string | null; version?: string; } export interface BrowserStatusPayload { claude: ClaudeBrowserStatus; codex: CodexBrowserStatus; } export interface BrowserDashboardPayload { config: BrowserSettingsConfig; status: BrowserStatusPayload; } export interface UpdateBrowserSettingsPayload { claude?: Partial; codex?: Partial; } export interface Profile { name: string; settingsPath: string; configured: boolean; target?: CliTarget; cliproxyBridge?: CliproxyBridgeMetadata | null; } export type LocalRuntimeStatus = 'ready' | 'missing-model' | 'offline'; export interface LocalRuntimeReadiness { id: 'ollama' | 'llamacpp'; name: string; endpoint: string; status: LocalRuntimeStatus; commandHint: string; recommendedModel: string | null; recommendedModelInstalled: boolean; detectedModelCount: number; } export interface LocalRuntimeReadinessResponse { runtimes: LocalRuntimeReadiness[]; } export interface CreateProfile { name: string; baseUrl: string; apiKey: string; model?: string; opusModel?: string; sonnetModel?: string; haikuModel?: string; target?: CliTarget; } export interface UpdateProfile { baseUrl?: string; apiKey?: string; model?: string; opusModel?: string; sonnetModel?: string; haikuModel?: string; target?: CliTarget; } export interface ProfileValidationIssue { level: 'error' | 'warning'; code: string; message: string; field?: string; hint?: string; } export interface ProfileValidationSummary { valid: boolean; issues: ProfileValidationIssue[]; } export interface ApiProfileOrphanCandidate { name: string; settingsPath: string; validation: ProfileValidationSummary; } export interface DiscoverProfileOrphansResponse { orphans: ApiProfileOrphanCandidate[]; } export interface RegisterProfileOrphansRequest { names?: string[]; target?: CliTarget; force?: boolean; } export interface RegisterProfileOrphansResponse { registered: string[]; skipped: Array<{ name: string; reason: string }>; } export interface CopyProfileRequest { destination: string; target?: CliTarget; force?: boolean; } export interface CopyProfileResponse { success: boolean; name?: string; settingsPath?: string; warnings?: string[]; } export interface ApiProfileExportBundle { schemaVersion: 1; exportedAt: string; profile: { name: string; target: CliTarget; }; settings: Record; } export interface ExportProfileResponse { success: boolean; bundle: ApiProfileExportBundle; redacted?: boolean; } export interface ImportProfileRequest { bundle: ApiProfileExportBundle; name?: string; target?: CliTarget; force?: boolean; } export interface ImportProfileResponse { success: boolean; name?: string; warnings?: string[]; validation?: ProfileValidationSummary; } export interface Variant { name: string; provider: CLIProxyProvider; settings: string; account?: string; port?: number; model?: string; target?: CliTarget; type?: 'composite'; default_tier?: 'opus' | 'sonnet' | 'haiku'; tiers?: { opus: { provider: string; model: string; account?: string; thinking?: string }; sonnet: { provider: string; model: string; account?: string; thinking?: string }; haiku: { provider: string; model: string; account?: string; thinking?: string }; }; } export interface CreateVariant { name: string; provider: CLIProxyProvider; model?: string; account?: string; target?: CliTarget; type?: 'composite'; default_tier?: 'opus' | 'sonnet' | 'haiku'; tiers?: { opus: { provider: string; model: string; account?: string; thinking?: string }; sonnet: { provider: string; model: string; account?: string; thinking?: string }; haiku: { provider: string; model: string; account?: string; thinking?: string }; }; } export interface UpdateVariant { provider?: CLIProxyProvider; model?: string; account?: string; target?: CliTarget; type?: 'composite'; default_tier?: 'opus' | 'sonnet' | 'haiku'; tiers?: { opus: { provider: string; model: string; account?: string; thinking?: string }; sonnet: { provider: string; model: string; account?: string; thinking?: string }; haiku: { provider: string; model: string; account?: string; thinking?: string }; }; } /** OAuth account info for multi-account support */ export interface OAuthAccount { id: string; email?: string; nickname?: string; provider: CLIProxyProvider; 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, pro, ultra, or unknown */ tier?: 'free' | 'pro' | 'ultra' | '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; } export type RoutingStrategy = 'round-robin' | 'fill-first'; export interface CliproxyRoutingState { strategy: RoutingStrategy; source: 'live' | 'config'; target: 'local' | 'remote'; reachable: boolean; message?: string; } export interface CliproxyRoutingApplyResult extends CliproxyRoutingState { applied: 'live' | 'live-and-config' | 'config-only'; } /** 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; } export interface CliproxyCatalogPresetMapping { default: string; opus: string; sonnet: string; haiku: string; } export interface CliproxyCatalogModel { id: string; name: string; tier?: 'free' | 'paid'; description?: string; broken?: boolean; issueUrl?: string; deprecated?: boolean; deprecationReason?: string; extendedContext?: boolean; presetMapping?: CliproxyCatalogPresetMapping; } export interface CliproxyModelRoutingHint { modelId: string; modelName: string; prefix: string; pinnedModelId: string; recommendedModelId: string; pinnedAvailable: boolean; unprefixedStatus: 'safe' | 'shadowed' | 'prefix-only'; effectiveProvider: string | null; effectiveDisplayName: string | null; effectiveOwnedBy: string | null; summary: string; } export interface CliproxyProviderRoutingHints { provider: string; displayName: string; prefix: string; safeCount: number; shadowedCount: number; prefixOnlyCount: number; models: CliproxyModelRoutingHint[]; } export interface CliproxyProviderCatalog { provider: string; displayName: string; models: CliproxyCatalogModel[]; defaultModel: string; } export interface CliproxyCatalogResponse { catalogs: Partial>; routing?: Partial>; source: 'live' | 'cache' | 'static'; cache: { synced: boolean; age: string | null; }; } /** 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; /** Upstream HTTP status when available */ httpStatus?: number; /** Stable machine-readable error code */ errorCode?: string; /** Additional provider-specific detail/code from upstream */ errorDetail?: string; /** True if account lacks quota access (403) */ isForbidden?: boolean; /** Error message if fetch failed */ error?: string; /** Provider-specific remediation guidance */ actionHint?: string; /** True when the failure is temporary and retrying later may help */ retryable?: boolean; /** True if token is expired and needs re-authentication */ needsReauth?: boolean; /** Richer provider entitlement evidence derived from live/runtime signals */ entitlement?: ProviderEntitlementEvidence; } /** 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; } /** Core Codex usage window (5h/weekly) extracted from raw windows */ export interface CodexCoreUsageWindow { /** Source window label */ label: string; /** Percentage remaining (0-100) */ remainingPercent: number; /** Seconds until quota resets, null if unknown */ resetAfterSeconds: number | null; /** ISO timestamp when quota resets, null if unknown */ resetAt: string | null; } /** Core Codex usage summary with explicit 5h and weekly windows */ export interface CodexCoreUsageSummary { /** Short-cycle usage limit window (typically 5h) */ fiveHour: CodexCoreUsageWindow | null; /** Long-cycle usage limit window (typically weekly) */ weekly: CodexCoreUsageWindow | null; } /** Codex quota result */ export interface CodexQuotaResult { /** Whether fetch succeeded */ success: boolean; /** Quota windows (primary, secondary, code review) */ windows: CodexQuotaWindow[]; /** Explicit core usage windows (5h + weekly) for easier reset display */ coreUsage?: CodexCoreUsageSummary; /** Plan type: free, plus, pro, team, or null if unknown */ planType: 'free' | 'plus' | 'pro' | 'team' | null; /** Timestamp of fetch */ lastUpdated: number; /** Upstream HTTP status when available */ httpStatus?: number; /** Stable machine-readable error code */ errorCode?: string; /** Additional provider-specific detail/code from upstream */ errorDetail?: string; /** Error message if fetch failed */ error?: string; /** Account ID (email) this quota belongs to */ accountId?: string; /** Provider-specific remediation guidance */ actionHint?: string; /** True if token is expired and needs re-authentication */ needsReauth?: boolean; /** True when the failure is temporary and retrying later may help */ retryable?: boolean; /** True if result was served from cache */ cached?: boolean; /** True if account lacks quota access (403) - displayed as 0% instead of error */ isForbidden?: boolean; } /** Claude policy limit window */ export interface ClaudeQuotaWindow { /** Source identifier: five_hour, seven_day, seven_day_opus, seven_day_sonnet, overage, ... */ rateLimitType: string; /** Human-friendly label for UI display */ label: string; /** Upstream status: allowed, allowed_warning, rejected */ status: string; /** Utilization ratio (0-1) when available */ utilization: number | null; /** Utilization as percentage (0-100) */ usedPercent: number; /** Remaining percentage (100 - usedPercent) */ remainingPercent: number; /** Reset timestamp for this window, null if unknown */ resetAt: string | null; surpassedThreshold?: boolean; severity?: string; overageStatus?: string; overageResetsAt?: string | null; overageDisabledReason?: string | null; isUsingOverage?: boolean; hasExtraUsageEnabled?: boolean; } /** Core Claude usage window (5h/weekly) */ export interface ClaudeCoreUsageWindow { rateLimitType: string; label: string; remainingPercent: number; resetAt: string | null; status: string; } /** Core Claude usage summary (5h + weekly) */ export interface ClaudeCoreUsageSummary { fiveHour: ClaudeCoreUsageWindow | null; weekly: ClaudeCoreUsageWindow | null; } /** Claude quota result */ export interface ClaudeQuotaResult { success: boolean; windows: ClaudeQuotaWindow[]; coreUsage?: ClaudeCoreUsageSummary; lastUpdated: number; httpStatus?: number; errorCode?: string; errorDetail?: string; isForbidden?: boolean; error?: string; accountId?: string; actionHint?: string; needsReauth?: boolean; retryable?: boolean; /** True if result was served from cache */ cached?: boolean; } /** 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; /** Remaining quota count when the upstream API provides it */ remainingAmount?: number | null; /** 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; /** Human-readable Gemini tier label when available */ tierLabel?: string | null; /** Stable Gemini tier identifier when available */ tierId?: string | null; /** Available Google One AI credits when reported by the API */ creditBalance?: number | null; /** Richer provider entitlement evidence derived from live/runtime signals */ entitlement?: ProviderEntitlementEvidence; /** Timestamp of fetch */ lastUpdated: number; /** Upstream HTTP status when available */ httpStatus?: number; /** Stable machine-readable error code */ errorCode?: string; /** Additional provider-specific detail/code from upstream */ errorDetail?: string; /** True if account lacks quota access (403) */ isForbidden?: boolean; /** Error message if fetch failed */ error?: string; /** Account ID (email) this quota belongs to */ accountId?: string; /** Provider-specific remediation guidance */ actionHint?: string; /** True if token is expired and needs re-authentication */ needsReauth?: boolean; /** True when the failure is temporary and retrying later may help */ retryable?: boolean; /** True if result was served from cache */ cached?: boolean; } /** GitHub Copilot quota snapshot */ export interface GhcpQuotaSnapshot { /** Total quota allocation for this category */ entitlement: number; /** Remaining quota count */ remaining: number; /** Used quota count */ used: number; /** Remaining quota percentage (0-100) */ percentRemaining: number; /** Used quota percentage (0-100) */ percentUsed: number; /** Whether this quota category is unlimited */ unlimited: boolean; /** Overage usage count */ overageCount: number; /** Whether overage is permitted */ overagePermitted: boolean; /** Upstream quota identifier if available */ quotaId: string | null; } /** GitHub Copilot (ghcp) quota result */ export interface GhcpQuotaResult { /** Whether fetch succeeded */ success: boolean; /** Copilot plan type */ planType: string | null; /** Quota reset date/time */ quotaResetDate: string | null; snapshots: { premiumInteractions: GhcpQuotaSnapshot; chat: GhcpQuotaSnapshot; completions: GhcpQuotaSnapshot; }; /** Timestamp of fetch */ lastUpdated: number; /** Upstream HTTP status when available */ httpStatus?: number; /** Stable machine-readable error code */ errorCode?: string; /** Additional provider-specific detail/code from upstream */ errorDetail?: string; /** True if account lacks quota access (403) */ isForbidden?: boolean; /** Error message if fetch failed */ error?: string; /** Account ID this quota belongs to */ accountId?: string; /** Provider-specific remediation guidance */ actionHint?: string; /** True if token is expired and needs re-authentication */ needsReauth?: boolean; /** True when the failure is temporary and retrying later may help */ retryable?: boolean; /** True if result was served from cache */ cached?: boolean; } /** Provider accounts summary */ export type ProviderAccountsMap = Record; export interface Account { name: string; type?: string; created: string; last_used?: string | null; context_mode?: 'isolated' | 'shared'; context_group?: string; continuity_mode?: 'standard' | 'deeper'; context_inferred?: boolean; continuity_inferred?: boolean; provider?: string; displayName?: string; } export interface PlainCcsLane { kind: 'native' | 'account-default' | 'account-inherited' | 'profile-default' | 'ambient'; label: string; account_name?: string | null; profile_name?: string | null; project_count: number; } export interface UpdateAccountContext { context_mode: 'isolated' | 'shared'; context_group?: string; continuity_mode?: 'standard' | 'deeper'; } export type LogsLevel = 'debug' | 'info' | 'warn' | 'error'; export interface LogsConfig { enabled: boolean; level: LogsLevel; rotate_mb: number; retain_days: number; redact: boolean; live_buffer_size: number; } export interface LogsSource { source: string; label: string; kind: 'native' | 'legacy'; count: number; lastTimestamp: string | null; } export interface LogsEntry { id: string; timestamp: string; level: LogsLevel; source: string; event: string; message: string; processId: number | null; runId: string | null; context?: unknown; } export interface LogsEntriesParams { source?: string; level?: LogsLevel; search?: string; limit?: number; } export type UpdateLogsConfigPayload = Partial; // 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; faultyRange?: { min: string; max: string; }; fromCache: boolean; checkedAt: number; } /** Result from installing a specific version */ export interface CliproxyInstallResult { success: boolean; version?: string; restarted?: boolean; port?: number; isFaulty?: boolean; isExperimental?: 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'), getLocalRuntimeReadiness: () => request('/profiles/local-runtime-readiness'), 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' }), discoverOrphans: () => request('/profiles/orphans'), registerOrphans: (data: RegisterProfileOrphansRequest) => request('/profiles/orphans/register', { method: 'POST', body: JSON.stringify(data), }), copy: (name: string, data: CopyProfileRequest) => request(`/profiles/${name}/copy`, { method: 'POST', body: JSON.stringify(data), }), export: (name: string, includeSecrets = false) => request(`/profiles/${name}/export`, { method: 'POST', body: JSON.stringify({ includeSecrets }), }), import: (data: ImportProfileRequest) => request('/profiles/import', { method: 'POST', body: JSON.stringify(data), }), }, imageAnalysis: { get: () => request('/image-analysis'), update: (data: UpdateImageAnalysisSettingsPayload) => request('/image-analysis', { method: 'PUT', body: JSON.stringify(data), }), }, browser: { get: () => request('/browser'), getStatus: () => request('/browser/status'), update: (data: UpdateBrowserSettingsPayload) => request<{ success: boolean; browser: BrowserDashboardPayload }>('/browser', { method: 'PUT', body: JSON.stringify(data), }), }, logs: { getConfig: () => request<{ logging: LogsConfig }>('/logs/config'), updateConfig: (data: UpdateLogsConfigPayload) => request<{ success: boolean; logging: LogsConfig }>('/logs/config', { method: 'PUT', body: JSON.stringify(data), }), getSources: () => request<{ sources: LogsSource[] }>('/logs/sources'), getEntries: ({ source, level, search, limit }: LogsEntriesParams = {}) => { const params = new URLSearchParams(); if (source) { params.set('source', source); } if (level) { params.set('level', level); } if (search) { params.set('search', search); } if (typeof limit === 'number') { params.set('limit', String(limit)); } const query = params.toString(); return request<{ entries: LogsEntry[] }>(`/logs/entries${query ? `?${query}` : ''}`); }, }, 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'), catalog: () => request('/cliproxy/catalog'), models: () => request('/cliproxy/models'), updateModel: (provider: string, model: string) => request(`/cliproxy/models/${provider}`, { method: 'PUT', body: JSON.stringify({ model }), }), getRoutingStrategy: () => request('/cliproxy/routing/strategy'), updateRoutingStrategy: (strategy: RoutingStrategy) => request('/cliproxy/routing/strategy', { method: 'PUT', body: JSON.stringify({ value: strategy }), }), aiProviders: { list: () => request('/cliproxy/ai-providers'), create: (family: AiProviderFamilyId, data: UpsertAiProviderEntryInput) => request(`/cliproxy/ai-providers/${encodeURIComponent(family)}`, { method: 'POST', body: JSON.stringify(data), }), update: (family: AiProviderFamilyId, entryId: string, data: UpsertAiProviderEntryInput) => request( `/cliproxy/ai-providers/${encodeURIComponent(family)}/${encodeURIComponent(entryId)}`, { method: 'PUT', body: JSON.stringify(data), } ), delete: (family: AiProviderFamilyId, entryId: string) => request( `/cliproxy/ai-providers/${encodeURIComponent(family)}/${encodeURIComponent(entryId)}`, { method: 'DELETE', } ), }, // Config YAML for Config tab getConfigYaml: async (): Promise => { const res = await fetch(withApiBase('/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(withApiBase('/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( withApiBase(`/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}/${encodeURIComponent(accountId)}`, { method: 'DELETE', }), pause: (provider: string, accountId: string) => request<{ provider: string; accountId: string; paused: boolean }>( `/cliproxy/auth/accounts/${provider}/${encodeURIComponent(accountId)}/pause`, { method: 'POST' } ), resume: (provider: string, accountId: string) => request<{ provider: string; accountId: string; paused: boolean }>( `/cliproxy/auth/accounts/${provider}/${encodeURIComponent(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(withApiBase(`/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; plain_ccs_lane?: PlainCcsLane }>( '/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' }), updateContext: (name: string, data: UpdateAccountContext) => request(`/accounts/${encodeURIComponent(name)}/context`, { method: 'PUT', body: JSON.stringify(data), }), }, // 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)}`), /** Fetch Codex quota for a specific account */ getCodex: (accountId: string) => request(`/cliproxy/quota/codex/${encodeURIComponent(accountId)}`), /** Fetch Claude quota for a specific account */ getClaude: (accountId: string) => request(`/cliproxy/quota/claude/${encodeURIComponent(accountId)}`), /** Fetch Gemini CLI quota for a specific account */ getGemini: (accountId: string) => request(`/cliproxy/quota/gemini/${encodeURIComponent(accountId)}`), }, };