| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import type { ApiClient } from "@homepilot/api-client"; |
| import type { |
| Device, |
| Job, |
| JobEvent, |
| ComputeStatus, |
| SupplierPolicy, |
| ComputeSource, |
| ComputeDevice, |
| ModelManifest, |
| ModelRoute, |
| ComputeSettings, |
| RouteResolution, |
| LocalResources, |
| SourceTestResult, |
| CloudSyncResult, |
| } from "@homepilot/types"; |
|
|
| export interface ImageJobInput { |
| model?: string; |
| prompt: string; |
| negativePrompt?: string; |
| width?: number; |
| height?: number; |
| steps?: number; |
| seed?: number; |
| } |
|
|
| export interface VideoJobInput { |
| model?: string; |
| prompt?: string; |
| image?: string; |
| } |
|
|
| |
| export interface EventTransport { |
| subscribe(url: string, onEvent: (event: JobEvent) => void): () => void; |
| } |
|
|
| export interface ComputeClient { |
| |
| createImageJob(input: ImageJobInput): Promise<Job>; |
| createVideoJob(input: VideoJobInput): Promise<Job>; |
| getJobStatus(id: string): Promise<Job>; |
| subscribeToJobEvents( |
| id: string, |
| onEvent: (event: JobEvent) => void, |
| transport: EventTransport, |
| ): () => void; |
| getComputeStatus(): Promise<ComputeStatus>; |
| |
| listUserDevices(): Promise<Device[]>; |
| getDevicePolicy(deviceId: string): Promise<SupplierPolicy>; |
| setDevicePolicy(deviceId: string, policy: Partial<SupplierPolicy>): Promise<SupplierPolicy>; |
|
|
| |
| listComputeSources(): Promise<ComputeSource[]>; |
| |
| upsertComputeSource( |
| source: Partial<ComputeSource> & { id: string; credential?: string }, |
| ): Promise<ComputeSource>; |
| deleteComputeSource(id: string): Promise<void>; |
| setSourceCredential(id: string, credential: string): Promise<{ credentialRef: string }>; |
| testComputeSource(id: string): Promise<SourceTestResult>; |
|
|
| listComputeDevices(sourceId?: string): Promise<ComputeDevice[]>; |
| getLocalResources(): Promise<LocalResources>; |
| |
| syncFromCloud(): Promise<CloudSyncResult>; |
|
|
| listModelManifests(): Promise<ModelManifest[]>; |
| upsertModelManifest(manifest: ModelManifest): Promise<ModelManifest>; |
| deleteModelManifest(id: string): Promise<void>; |
|
|
| listRoutes(): Promise<ModelRoute[]>; |
| upsertRoute(route: ModelRoute): Promise<ModelRoute>; |
| deleteRoute(modality: string, modelId: string): Promise<void>; |
|
|
| getComputeSettings(): Promise<ComputeSettings>; |
| putComputeSettings(patch: Partial<ComputeSettings>): Promise<ComputeSettings>; |
|
|
| resolveRoute(modality: string, modelId?: string): Promise<RouteResolution>; |
| } |
|
|
| export function createComputeClient(api: ApiClient): ComputeClient { |
| |
| |
| |
| |
| const get = <T>(path: string) => api.get<unknown>(path).then((r) => camelize(r) as T); |
| const post = <T>(path: string, body?: unknown) => |
| api.post<unknown>(path, body).then((r) => camelize(r) as T); |
| const put = <T>(path: string, body?: unknown) => |
| api.put<unknown>(path, body).then((r) => camelize(r) as T); |
| const del = (path: string) => api.del<unknown>(path).then(() => undefined); |
|
|
| return { |
| createImageJob: (input) => post<Job>("/v1/images/generations", toSnake(input)), |
| createVideoJob: (input) => post<Job>("/v1/videos/generations", toSnake(input)), |
| getJobStatus: (id) => get<Job>(`/v1/jobs/${id}`), |
| subscribeToJobEvents: (id, onEvent, transport) => |
| transport.subscribe(`${api.baseUrl}/v1/jobs/${id}/events`, onEvent), |
| getComputeStatus: () => get<ComputeStatus>("/compute/status"), |
| listUserDevices: () => get<Device[]>("/v1/devices"), |
| getDevicePolicy: (deviceId) => get<SupplierPolicy>(`/v1/devices/${deviceId}/policy`), |
| setDevicePolicy: (deviceId, policy) => |
| put<SupplierPolicy>(`/v1/devices/${deviceId}/policy`, toSnake(policy)), |
|
|
| |
| listComputeSources: () => |
| get<{ sources: ComputeSource[] }>("/compute/admin/sources").then((r) => r.sources), |
| upsertComputeSource: (source) => |
| post<{ source: ComputeSource }>("/compute/admin/sources", deepSnake(source)).then( |
| (r) => r.source, |
| ), |
| deleteComputeSource: (id) => del(`/compute/admin/sources/${encodeURIComponent(id)}`), |
| setSourceCredential: (id, credential) => |
| post<{ credentialRef: string }>( |
| `/compute/admin/sources/${encodeURIComponent(id)}/credential`, |
| { credential }, |
| ), |
| testComputeSource: (id) => |
| post<SourceTestResult>(`/compute/admin/sources/${encodeURIComponent(id)}/test`), |
|
|
| listComputeDevices: (sourceId) => |
| get<{ devices: ComputeDevice[] }>( |
| `/compute/admin/devices${sourceId ? `?source_id=${encodeURIComponent(sourceId)}` : ""}`, |
| ).then((r) => r.devices), |
| getLocalResources: () => get<LocalResources>("/v1/system/resources"), |
| syncFromCloud: () => post<CloudSyncResult>("/compute/admin/sync"), |
|
|
| listModelManifests: () => |
| get<{ models: ModelManifest[] }>("/compute/admin/models").then((r) => r.models), |
| upsertModelManifest: (manifest) => |
| post<{ model: ModelManifest }>("/compute/admin/models", deepSnake(manifest)).then( |
| (r) => r.model, |
| ), |
| deleteModelManifest: (id) => del(`/compute/admin/models/${encodeURIComponent(id)}`), |
|
|
| listRoutes: () => |
| get<{ routes: ModelRoute[] }>("/compute/admin/routes").then((r) => r.routes), |
| upsertRoute: (route) => |
| post<{ route: ModelRoute }>("/compute/admin/routes", deepSnake(route)).then( |
| (r) => r.route, |
| ), |
| deleteRoute: (modality, modelId) => |
| del( |
| `/compute/admin/routes/${encodeURIComponent(modality)}/${encodeURIComponent(modelId)}`, |
| ), |
|
|
| getComputeSettings: () => |
| get<{ settings: ComputeSettings }>("/compute/admin/settings").then((r) => r.settings), |
| putComputeSettings: (patch) => |
| put<{ settings: ComputeSettings }>("/compute/admin/settings", deepSnake(patch)).then( |
| (r) => r.settings, |
| ), |
|
|
| resolveRoute: (modality, modelId) => |
| get<RouteResolution>( |
| `/compute/admin/resolve?modality=${encodeURIComponent(modality)}` + |
| (modelId ? `&model_id=${encodeURIComponent(modelId)}` : ""), |
| ), |
| }; |
| } |
|
|
| const toCamel = (key: string): string => |
| key.replace(/_([a-z0-9])/g, (_m, c: string) => c.toUpperCase()); |
|
|
| |
| function camelize(value: unknown): unknown { |
| if (Array.isArray(value)) return value.map(camelize); |
| if (value && typeof value === "object") { |
| const out: Record<string, unknown> = {}; |
| for (const [k, v] of Object.entries(value as Record<string, unknown>)) { |
| out[toCamel(k)] = camelize(v); |
| } |
| return out; |
| } |
| return value; |
| } |
|
|
| |
| function toSnake(obj: object): Record<string, unknown> { |
| const out: Record<string, unknown> = {}; |
| for (const [key, value] of Object.entries(obj)) { |
| if (value === undefined) continue; |
| out[key.replace(/[A-Z]/g, (m) => `_${m.toLowerCase()}`)] = value; |
| } |
| return out; |
| } |
|
|
| const snakeKey = (key: string): string => key.replace(/[A-Z]/g, (m) => `_${m.toLowerCase()}`); |
|
|
| |
| |
| function deepSnake(value: unknown): unknown { |
| if (Array.isArray(value)) return value.map(deepSnake); |
| if (value && typeof value === "object") { |
| const out: Record<string, unknown> = {}; |
| for (const [k, v] of Object.entries(value as Record<string, unknown>)) { |
| if (v === undefined) continue; |
| out[snakeKey(k)] = deepSnake(v); |
| } |
| return out; |
| } |
| return value; |
| } |
|
|