mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-15 20:20:09 +00:00
1475 lines
43 KiB
TypeScript
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)}`),
|
|
},
|
|
};
|