Files
ccs/ui/src/lib/api-client.ts
T
kaitranntt bf3d51ade3 feat(ui): add kiro and ghcp to provider types and configs
- update Variant, CreateVariant, UpdateVariant, OAuthAccount types

- add ghcp to PROVIDER_ASSETS, PROVIDER_COLORS, PROVIDER_NAMES

- add ghcp catalog with model configurations
2025-12-22 00:46:51 -05:00

408 lines
11 KiB
TypeScript

/**
* API Client
* Phase 03: REST API Routes & CRUD
*/
const BASE_URL = '/api';
async function request<T>(url: string, options?: RequestInit): Promise<T> {
const res = await fetch(`${BASE_URL}${url}`, {
headers: { 'Content-Type': 'application/json' },
...options,
});
if (!res.ok) {
const error = await res.json().catch(() => ({ error: 'Unknown error' }));
throw new Error(error.error || res.statusText);
}
return res.json();
}
// Types
export interface Profile {
name: string;
settingsPath: string;
configured: boolean;
}
export interface CreateProfile {
name: string;
baseUrl: string;
apiKey: string;
model?: string;
opusModel?: string;
sonnetModel?: string;
haikuModel?: string;
}
export interface UpdateProfile {
baseUrl?: string;
apiKey?: string;
model?: string;
opusModel?: string;
sonnetModel?: string;
haikuModel?: string;
}
export interface Variant {
name: string;
provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp';
settings: string;
account?: string;
}
export interface CreateVariant {
name: string;
provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp';
model?: string;
account?: string;
}
export interface UpdateVariant {
provider?: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp';
model?: string;
account?: string;
}
/** OAuth account info for multi-account support */
export interface OAuthAccount {
id: string;
email?: string;
provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp';
isDefault: boolean;
tokenFile: string;
createdAt: string;
lastUsedAt?: string;
}
export interface AuthStatus {
provider: string;
displayName: string;
authenticated: boolean;
lastAuth: string | null;
tokenFiles: number;
accounts: OAuthAccount[];
defaultAccount?: string;
}
/** Auth file info for Config tab */
export interface AuthFile {
name: string;
provider?: string;
}
/** CLIProxy model from /v1/models endpoint */
export interface CliproxyModel {
id: string;
object: string;
created: number;
owned_by: string;
}
/** Categorized models response from CLIProxyAPI */
export interface CliproxyModelsResponse {
models: CliproxyModel[];
byCategory: Record<string, CliproxyModel[]>;
totalCount: number;
}
/** Provider accounts summary */
export type ProviderAccountsMap = Record<string, OAuthAccount[]>;
export interface Account {
name: string;
type?: string;
created: string;
last_used?: string | null;
}
// Unified config types
export interface ConfigFormat {
format: 'yaml' | 'json' | 'none';
migrationNeeded: boolean;
backups: string[];
}
export interface MigrationResult {
success: boolean;
backupPath?: string;
error?: string;
migratedFiles: string[];
warnings: string[];
}
/** Model preset for quick model switching */
export interface ModelPreset {
name: string;
default: string;
opus: string;
sonnet: string;
haiku: string;
}
export interface CreatePreset {
name: string;
default: string;
opus?: string;
sonnet?: string;
haiku?: string;
}
/** Remote proxy status from health check */
export interface RemoteProxyStatus {
reachable: boolean;
latencyMs?: number;
error?: string;
errorCode?: 'CONNECTION_REFUSED' | 'TIMEOUT' | 'AUTH_FAILED' | 'UNKNOWN';
}
/** Remote proxy configuration */
export interface ProxyRemoteConfig {
enabled: boolean;
host: string;
/** Port is optional - uses protocol default (443 for HTTPS, 80 for HTTP) */
port?: number;
protocol: 'http' | 'https';
auth_token: string;
}
/** 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
}
// API
export const api = {
profiles: {
list: () => request<{ profiles: Profile[] }>('/profiles'),
create: (data: CreateProfile) =>
request('/profiles', {
method: 'POST',
body: JSON.stringify(data),
}),
update: (name: string, data: UpdateProfile) =>
request(`/profiles/${name}`, {
method: 'PUT',
body: JSON.stringify(data),
}),
delete: (name: string) => request(`/profiles/${name}`, { method: 'DELETE' }),
},
cliproxy: {
list: () => request<{ variants: Variant[] }>('/cliproxy'),
getAuthStatus: () =>
request<{ authStatus: AuthStatus[]; source?: 'remote' | 'local' }>('/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'),
// Stats and models for Overview tab
stats: () => request<{ usage: Record<string, unknown> }>('/cliproxy/usage'),
models: () => request<CliproxyModelsResponse>('/cliproxy/models'),
updateModel: (provider: string, model: string) =>
request(`/cliproxy/models/${provider}`, {
method: 'PUT',
body: JSON.stringify({ model }),
}),
// Config YAML for Config tab
getConfigYaml: async (): Promise<string> => {
const res = await fetch(`${BASE_URL}/cliproxy/config.yaml`);
if (!res.ok) throw new Error('Failed to load config');
return res.text();
},
saveConfigYaml: async (content: string): Promise<void> => {
const res = await fetch(`${BASE_URL}/cliproxy/config.yaml`, {
method: 'PUT',
headers: { 'Content-Type': 'application/yaml' },
body: content,
});
if (!res.ok) {
const error = await res.json().catch(() => ({ error: 'Failed to save config' }));
throw new Error(error.error || 'Failed to save config');
}
},
// Auth files for Config tab
getAuthFiles: () => request<{ files: AuthFile[] }>('/cliproxy/auth-files'),
getAuthFile: async (name: string): Promise<string> => {
const res = await fetch(
`${BASE_URL}/cliproxy/auth-files/download?name=${encodeURIComponent(name)}`
);
if (!res.ok) throw new Error('Failed to load auth file');
return res.text();
},
// Multi-account management
accounts: {
list: () => request<{ accounts: ProviderAccountsMap }>('/cliproxy/auth/accounts'),
listByProvider: (provider: string) =>
request<{ provider: string; accounts: OAuthAccount[] }>(
`/cliproxy/auth/accounts/${provider}`
),
setDefault: (provider: string, accountId: string) =>
request(`/cliproxy/auth/accounts/${provider}/default`, {
method: 'POST',
body: JSON.stringify({ accountId }),
}),
remove: (provider: string, accountId: string) =>
request(`/cliproxy/auth/accounts/${provider}/${accountId}`, { method: 'DELETE' }),
},
// 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 }),
}),
},
// 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(`${BASE_URL}/cliproxy/error-logs/${encodeURIComponent(name)}`);
if (!res.ok) throw new Error('Failed to load error log');
return res.text();
},
},
},
accounts: {
list: () => request<{ accounts: Account[]; default: string | null }>('/accounts'),
setDefault: (name: string) =>
request('/accounts/default', {
method: 'POST',
body: JSON.stringify({ name }),
}),
},
// 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),
}),
/** 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),
}),
},
};