Files
ccs/ui/src/lib/api-client.ts
T

1475 lines
43 KiB
TypeScript

/**
* 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<string> {
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<T>(url: string, options?: RequestInit): Promise<T> {
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<string, string>;
fallbackBackend: string | null;
profileBackends: Record<string, string>;
}
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<string, string | null>;
fallbackBackend?: string | null;
profileBackends?: Record<string, string>;
}
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<BrowserSettingsConfig['claude']>;
codex?: Partial<BrowserSettingsConfig['codex']>;
}
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<string, unknown>;
}
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<string, CliproxyModel[]>;
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<Record<string, CliproxyProviderCatalog>>;
routing?: Partial<Record<string, CliproxyProviderRoutingHints>>;
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<string, OAuthAccount[]>;
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<LogsConfig>;
// 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<LocalRuntimeReadinessResponse>('/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<DiscoverProfileOrphansResponse>('/profiles/orphans'),
registerOrphans: (data: RegisterProfileOrphansRequest) =>
request<RegisterProfileOrphansResponse>('/profiles/orphans/register', {
method: 'POST',
body: JSON.stringify(data),
}),
copy: (name: string, data: CopyProfileRequest) =>
request<CopyProfileResponse>(`/profiles/${name}/copy`, {
method: 'POST',
body: JSON.stringify(data),
}),
export: (name: string, includeSecrets = false) =>
request<ExportProfileResponse>(`/profiles/${name}/export`, {
method: 'POST',
body: JSON.stringify({ includeSecrets }),
}),
import: (data: ImportProfileRequest) =>
request<ImportProfileResponse>('/profiles/import', {
method: 'POST',
body: JSON.stringify(data),
}),
},
imageAnalysis: {
get: () => request<ImageAnalysisDashboardData>('/image-analysis'),
update: (data: UpdateImageAnalysisSettingsPayload) =>
request<ImageAnalysisDashboardData>('/image-analysis', {
method: 'PUT',
body: JSON.stringify(data),
}),
},
browser: {
get: () => request<BrowserDashboardPayload>('/browser'),
getStatus: () => request<BrowserStatusPayload>('/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<ProxyProcessStatus>('/cliproxy/proxy-status'),
proxyStart: () => request<ProxyStartResult>('/cliproxy/proxy-start', { method: 'POST' }),
proxyStop: () => request<ProxyStopResult>('/cliproxy/proxy-stop', { method: 'POST' }),
updateCheck: () => request<CliproxyUpdateCheckResult>('/cliproxy/update-check'),
// Version management
versions: () => request<CliproxyVersionsResponse>('/cliproxy/versions'),
install: (version: string, force?: boolean) =>
request<CliproxyInstallResult>('/cliproxy/install', {
method: 'POST',
body: JSON.stringify({ version, force }),
}),
restart: () => request<CliproxyRestartResult>('/cliproxy/restart', { method: 'POST' }),
// Stats and models for Overview tab
stats: () => request<{ usage: Record<string, unknown> }>('/cliproxy/usage'),
catalog: () => request<CliproxyCatalogResponse>('/cliproxy/catalog'),
models: () => request<CliproxyModelsResponse>('/cliproxy/models'),
updateModel: (provider: string, model: string) =>
request(`/cliproxy/models/${provider}`, {
method: 'PUT',
body: JSON.stringify({ model }),
}),
getRoutingStrategy: () => request<CliproxyRoutingState>('/cliproxy/routing/strategy'),
updateRoutingStrategy: (strategy: RoutingStrategy) =>
request<CliproxyRoutingApplyResult>('/cliproxy/routing/strategy', {
method: 'PUT',
body: JSON.stringify({ value: strategy }),
}),
aiProviders: {
list: () => request<ListAiProvidersResult>('/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<string> => {
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<void> => {
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<string> => {
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<string> => {
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<ConfigFormat>('/config/format'),
get: () => request<Record<string, unknown>>('/config'),
update: (config: Record<string, unknown>) =>
request<{ success: boolean }>('/config', {
method: 'PUT',
body: JSON.stringify(config),
}),
migrate: (dryRun = false) =>
request<MigrationResult>(`/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<CliproxyServerConfig>('/cliproxy-server'),
/** Update cliproxy server configuration */
update: (config: Partial<CliproxyServerConfig>) =>
request<CliproxyServerConfig>('/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<RemoteProxyStatus>('/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<QuotaResult>(`/cliproxy/quota/${provider}/${encodeURIComponent(accountId)}`),
/** Fetch Codex quota for a specific account */
getCodex: (accountId: string) =>
request<CodexQuotaResult>(`/cliproxy/quota/codex/${encodeURIComponent(accountId)}`),
/** Fetch Claude quota for a specific account */
getClaude: (accountId: string) =>
request<ClaudeQuotaResult>(`/cliproxy/quota/claude/${encodeURIComponent(accountId)}`),
/** Fetch Gemini CLI quota for a specific account */
getGemini: (accountId: string) =>
request<GeminiCliQuotaResult>(`/cliproxy/quota/gemini/${encodeURIComponent(accountId)}`),
},
};