import { normalizeResponseError } from './response-error' import { backendFetch } from './backend' import type { components, paths } from '../generated/backend-openapi' type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' type OperationFor< TPath extends keyof paths, TMethod extends HttpMethod, > = NonNullable type ResponsesFor< TPath extends keyof paths, TMethod extends HttpMethod, > = OperationFor['responses'] type JsonBodyOf = TResponse extends { content: infer TContent } ? TContent extends { 'application/json': infer TJson } ? TJson : never : never type JsonResponseFor< TPath extends keyof paths, TMethod extends HttpMethod, > = OperationFor extends { responses: { 200: infer TResponse } } ? JsonBodyOf : never type JsonBodyFor< TPath extends keyof paths, TMethod extends HttpMethod, > = OperationFor extends { requestBody?: { content: { 'application/json': infer TBody } } } ? TBody : never type QueryFor< TPath extends keyof paths, TMethod extends HttpMethod, > = OperationFor extends { parameters: { query?: infer TQuery } } ? TQuery : never type HTTPErrorResponse = components["schemas"]["HTTPErrorResponse"] type ExactErrorResponseFor< TPath extends keyof paths, TMethod extends HttpMethod, TStatus extends number, > = TStatus extends keyof ResponsesFor ? JsonBodyOf[TStatus]> : never type Fallback4xxErrorFor< TPath extends keyof paths, TMethod extends HttpMethod, > = '4XX' extends keyof ResponsesFor ? JsonBodyOf['4XX']> : HTTPErrorResponse type Fallback5xxErrorFor< TPath extends keyof paths, TMethod extends HttpMethod, > = '5XX' extends keyof ResponsesFor ? JsonBodyOf['5XX']> : HTTPErrorResponse type DefaultErrorFor< TPath extends keyof paths, TMethod extends HttpMethod, > = 'default' extends keyof ResponsesFor ? JsonBodyOf['default']> : HTTPErrorResponse type ExactErrorMembers< TPath extends keyof paths, TMethod extends HttpMethod, TExactStatuses extends readonly number[], > = { [TStatus in TExactStatuses[number]]: { ok: false status: TStatus error: ExactErrorResponseFor } }[TExactStatuses[number]] type FallbackErrorMembers< TPath extends keyof paths, TMethod extends HttpMethod, > = | { ok: false status: '4XX' error: Fallback4xxErrorFor } | { ok: false status: '5XX' error: Fallback5xxErrorFor } | { ok: false status: 'default' error: DefaultErrorFor } export type EndpointResult< TPath extends keyof paths, TMethod extends HttpMethod, TExactStatuses extends readonly number[] = [], > = | { ok: true data: JsonResponseFor } | ExactErrorMembers | FallbackErrorMembers type SyntheticErrorStatus = '4XX' | '5XX' | 'default' export type ApiSuccess = TValue extends { ok: true; data: infer TData } ? TData : never export type ApiErrors = TValue extends { ok: false; status: infer TStatus; error: infer TError } ? { status: TStatus; error: TError } : never function buildQueryString(query: Record | undefined): string { if (!query) return '' const params = new URLSearchParams() for (const [key, value] of Object.entries(query)) { if (value === null || value === undefined) continue params.set(key, String(value)) } const serialized = params.toString() return serialized ? `?${serialized}` : '' } function buildJsonRequestInit(body: unknown, init?: RequestInit): RequestInit { const headers = new Headers(init?.headers) headers.set('Content-Type', 'application/json') return { ...init, headers, body: JSON.stringify(body), } } function buildSyntheticError(code: string, message: string): HTTPErrorResponse { return { code, message } } function resolveFallbackStatus(httpStatus: number): SyntheticErrorStatus { if (httpStatus >= 400 && httpStatus < 500) return '4XX' if (httpStatus >= 500 && httpStatus < 600) return '5XX' return 'default' } function resolveErrorStatus( httpStatus: number, exactErrorStatuses: TExactStatuses, ): TExactStatuses[number] | SyntheticErrorStatus { if ((exactErrorStatuses as readonly number[]).includes(httpStatus)) { return httpStatus as TExactStatuses[number] } return resolveFallbackStatus(httpStatus) } function buildParsedErrorResult< TPath extends keyof paths, TMethod extends HttpMethod, TExactStatuses extends readonly number[], >( status: TExactStatuses[number] | SyntheticErrorStatus, payload: unknown, ): EndpointResult { const normalized = normalizeResponseError(payload) return { ok: false, status: status === 402 && normalized.code !== 'LTX_INSUFFICIENT_FUNDS' ? '4XX' : status, error: normalized as ExactErrorResponseFor | Fallback4xxErrorFor | Fallback5xxErrorFor | DefaultErrorFor, } as EndpointResult } function buildSyntheticErrorResult< TPath extends keyof paths, TMethod extends HttpMethod, TExactStatuses extends readonly number[], >( status: SyntheticErrorStatus, code: string, message: string, ): EndpointResult { return { ok: false, status, error: buildSyntheticError(code, message) as Fallback4xxErrorFor | Fallback5xxErrorFor | DefaultErrorFor, } as EndpointResult } async function requestEndpointResult< TPath extends keyof paths, TMethod extends HttpMethod, TExactStatuses extends readonly number[], >( endpoint: TPath, method: TMethod, exactErrorStatuses: TExactStatuses, init?: RequestInit, requestPath?: string, ): Promise> { const path = requestPath ?? String(endpoint) let response: Response try { response = await backendFetch(path, { method: method.toUpperCase(), ...init, }) } catch (error) { return buildSyntheticErrorResult( 'default', 'NETWORK_ERROR', error instanceof Error ? error.message : 'Request failed before the server responded.', ) } let text = '' try { text = await response.text() } catch (error) { return buildSyntheticErrorResult( resolveFallbackStatus(response.status), 'RESPONSE_READ_FAILED', error instanceof Error ? error.message : 'Failed to read response body.', ) } if (response.ok) { if (!text) { return buildSyntheticErrorResult( 'default', 'EMPTY_SUCCESS_RESPONSE', `${path} returned an empty response body.`, ) } try { return { ok: true, data: JSON.parse(text) as JsonResponseFor, } } catch (error) { return buildSyntheticErrorResult( 'default', 'INVALID_SUCCESS_RESPONSE', error instanceof Error ? error.message : 'Server returned invalid JSON.', ) } } if (!text) { return buildSyntheticErrorResult( resolveFallbackStatus(response.status), `HTTP_${response.status}`, `${response.status} ${response.statusText || 'Request failed'}`, ) } try { const payload = JSON.parse(text) as unknown return buildParsedErrorResult( resolveErrorStatus(response.status, exactErrorStatuses), payload, ) } catch { return buildSyntheticErrorResult( resolveFallbackStatus(response.status), `HTTP_${response.status}`, text, ) } } export function makeEndpointClient< TPath extends keyof paths, TMethod extends HttpMethod, TExactStatuses extends readonly number[] = [], >( endpoint: TPath, method: TMethod, config?: { exactErrorStatuses?: TExactStatuses }, ) { const exactErrorStatuses = (config?.exactErrorStatuses ?? []) as TExactStatuses return ( body?: JsonBodyFor, init?: RequestInit, requestPath?: string, ): Promise> => { const requestInit = body === undefined ? init : buildJsonRequestInit(body, init) return requestEndpointResult(endpoint, method, exactErrorStatuses, requestInit, requestPath) } } export class ApiClient { static getHealth = makeEndpointClient('/health', 'get') static getModelDownloadProgress( query: QueryFor<'/api/models/download/progress', 'get'>, ): Promise> { const path = `/api/models/download/progress${buildQueryString(query as Record)}` return requestEndpointResult('/api/models/download/progress', 'get', [] as const, undefined, path) } static listModels( query?: QueryFor<'/api/models', 'get'>, ): Promise> { const path = `/api/models${buildQueryString(query as Record)}` return requestEndpointResult('/api/models', 'get', [] as const, undefined, path) } static getLtxRecommendation = makeEndpointClient('/api/models/ltx-recommendation', 'get') static getImgGenRecommendation = makeEndpointClient('/api/models/img-gen-recommendation', 'get') static getLtxIcLoraRecommendation = makeEndpointClient('/api/models/ltx-ic-lora-recommendation', 'get') static getTextEncoderRecommendation = makeEndpointClient('/api/models/text-encoder-recommendation', 'get') static describeCheckpoints = makeEndpointClient('/api/models/describe', 'post') static getActiveDownload = makeEndpointClient('/api/models/download/active', 'get') static getLtxVersions = makeEndpointClient('/api/models/ltx-versions', 'get') static setActiveLtxModel = makeEndpointClient('/api/models/active-ltx-model', 'post') static startModelDownload = makeEndpointClient('/api/models/download', 'post') static deleteModels = makeEndpointClient('/api/models/delete', 'delete') static getRuntimePolicy = makeEndpointClient('/api/runtime-policy', 'get') static getGpuInfo = makeEndpointClient('/api/gpu-info', 'get') static getSettings = makeEndpointClient('/api/settings', 'get') static listGeminiModels = makeEndpointClient('/api/settings/gemini-models', 'get') static updateSettings = makeEndpointClient('/api/settings', 'post') static suggestGapPrompt = makeEndpointClient('/api/suggest-gap-prompt', 'post', { exactErrorStatuses: [401, 403] as const, }) static generateVideo = makeEndpointClient('/api/generate', 'post', { exactErrorStatuses: [402] as const, }) static getGenerateVideoModelSpecs = makeEndpointClient('/api/generate/models-specs', 'get') static cancelGeneration = makeEndpointClient('/api/generate/cancel', 'post') static getGenerationProgress = makeEndpointClient('/api/generation/progress', 'get') static generateImage = makeEndpointClient('/api/generate-image', 'post') static enhancePrompt = makeEndpointClient('/api/enhance-prompt', 'post', { exactErrorStatuses: [404, 409] as const, }) static retake = makeEndpointClient('/api/retake', 'post') static extend = makeEndpointClient('/api/extend', 'post') static startHuggingFaceLogin = makeEndpointClient('/api/auth/huggingface/login', 'post') static getHuggingFaceAuthStatus = makeEndpointClient('/api/auth/huggingface/status', 'get') static huggingFaceLogout = makeEndpointClient('/api/auth/huggingface/logout', 'post') static checkModelAccess = makeEndpointClient('/api/models/check-access', 'post') static generateIcLora = makeEndpointClient('/api/ic-lora/generate', 'post') static extractIcLoraConditioning = makeEndpointClient('/api/ic-lora/extract-conditioning', 'post') static listIcLoras = makeEndpointClient('/api/ic-loras', 'get') static startIcLoraDownload = makeEndpointClient('/api/ic-loras/download', 'post') static getIcLoraDownloadProgress( query: QueryFor<'/api/ic-loras/download/progress', 'get'>, ): Promise> { const path = `/api/ic-loras/download/progress${buildQueryString(query as Record)}` return requestEndpointResult('/api/ic-loras/download/progress', 'get', [] as const, undefined, path) } static listLoras = makeEndpointClient('/api/loras', 'get') static startLoraDownload = makeEndpointClient('/api/loras/download', 'post') static getLoraDownloadProgress( query: QueryFor<'/api/loras/download/progress', 'get'>, ): Promise> { const path = `/api/loras/download/progress${buildQueryString(query as Record)}` return requestEndpointResult('/api/loras/download/progress', 'get', [] as const, undefined, path) } } type ApiClientMethodName = keyof typeof ApiClient export type ApiRequestBodyOf = (typeof ApiClient)[TMethod] extends ( body?: infer TBody, ...args: any[] ) => Promise ? TBody : never export type ApiSuccessOf = (typeof ApiClient)[TMethod] extends (...args: any[]) => Promise ? ApiSuccess>> : never export type ApiErrorsOf = (typeof ApiClient)[TMethod] extends (...args: any[]) => Promise ? ApiErrors>> : never