Create standalone browser port of LTX Athena with authenticated native API transport

This commit is contained in:
Mikei386
2026-09-29 17:59:23 +02:00
commit 1333b0c36b
202 changed files with 63054 additions and 0 deletions
+446
View File
@@ -0,0 +1,446 @@
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<paths[TPath][TMethod]>
type ResponsesFor<
TPath extends keyof paths,
TMethod extends HttpMethod,
> = OperationFor<TPath, TMethod>['responses']
type JsonBodyOf<TResponse> = TResponse extends {
content: infer TContent
}
? TContent extends { 'application/json': infer TJson }
? TJson
: never
: never
type JsonResponseFor<
TPath extends keyof paths,
TMethod extends HttpMethod,
> = OperationFor<TPath, TMethod> extends {
responses: { 200: infer TResponse }
}
? JsonBodyOf<TResponse>
: never
type JsonBodyFor<
TPath extends keyof paths,
TMethod extends HttpMethod,
> = OperationFor<TPath, TMethod> extends {
requestBody?: { content: { 'application/json': infer TBody } }
}
? TBody
: never
type QueryFor<
TPath extends keyof paths,
TMethod extends HttpMethod,
> = OperationFor<TPath, TMethod> 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<TPath, TMethod>
? JsonBodyOf<ResponsesFor<TPath, TMethod>[TStatus]>
: never
type Fallback4xxErrorFor<
TPath extends keyof paths,
TMethod extends HttpMethod,
> = '4XX' extends keyof ResponsesFor<TPath, TMethod>
? JsonBodyOf<ResponsesFor<TPath, TMethod>['4XX']>
: HTTPErrorResponse
type Fallback5xxErrorFor<
TPath extends keyof paths,
TMethod extends HttpMethod,
> = '5XX' extends keyof ResponsesFor<TPath, TMethod>
? JsonBodyOf<ResponsesFor<TPath, TMethod>['5XX']>
: HTTPErrorResponse
type DefaultErrorFor<
TPath extends keyof paths,
TMethod extends HttpMethod,
> = 'default' extends keyof ResponsesFor<TPath, TMethod>
? JsonBodyOf<ResponsesFor<TPath, TMethod>['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<TPath, TMethod, TStatus>
}
}[TExactStatuses[number]]
type FallbackErrorMembers<
TPath extends keyof paths,
TMethod extends HttpMethod,
> =
| {
ok: false
status: '4XX'
error: Fallback4xxErrorFor<TPath, TMethod>
}
| {
ok: false
status: '5XX'
error: Fallback5xxErrorFor<TPath, TMethod>
}
| {
ok: false
status: 'default'
error: DefaultErrorFor<TPath, TMethod>
}
export type EndpointResult<
TPath extends keyof paths,
TMethod extends HttpMethod,
TExactStatuses extends readonly number[] = [],
> =
| {
ok: true
data: JsonResponseFor<TPath, TMethod>
}
| ExactErrorMembers<TPath, TMethod, TExactStatuses>
| FallbackErrorMembers<TPath, TMethod>
type SyntheticErrorStatus = '4XX' | '5XX' | 'default'
export type ApiSuccess<TValue> = TValue extends { ok: true; data: infer TData }
? TData
: never
export type ApiErrors<TValue> = TValue extends { ok: false; status: infer TStatus; error: infer TError }
? { status: TStatus; error: TError }
: never
function buildQueryString(query: Record<string, unknown> | 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<TExactStatuses extends readonly number[]>(
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<TPath, TMethod, TExactStatuses> {
const normalized = normalizeResponseError(payload)
return {
ok: false,
status: status === 402 && normalized.code !== 'LTX_INSUFFICIENT_FUNDS' ? '4XX' : status,
error: normalized as ExactErrorResponseFor<TPath, TMethod, TExactStatuses[number]>
| Fallback4xxErrorFor<TPath, TMethod>
| Fallback5xxErrorFor<TPath, TMethod>
| DefaultErrorFor<TPath, TMethod>,
} as EndpointResult<TPath, TMethod, TExactStatuses>
}
function buildSyntheticErrorResult<
TPath extends keyof paths,
TMethod extends HttpMethod,
TExactStatuses extends readonly number[],
>(
status: SyntheticErrorStatus,
code: string,
message: string,
): EndpointResult<TPath, TMethod, TExactStatuses> {
return {
ok: false,
status,
error: buildSyntheticError(code, message) as Fallback4xxErrorFor<TPath, TMethod>
| Fallback5xxErrorFor<TPath, TMethod>
| DefaultErrorFor<TPath, TMethod>,
} as EndpointResult<TPath, TMethod, TExactStatuses>
}
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<EndpointResult<TPath, TMethod, TExactStatuses>> {
const path = requestPath ?? String(endpoint)
let response: Response
try {
response = await backendFetch(path, {
method: method.toUpperCase(),
...init,
})
} catch (error) {
return buildSyntheticErrorResult<TPath, TMethod, TExactStatuses>(
'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<TPath, TMethod, TExactStatuses>(
resolveFallbackStatus(response.status),
'RESPONSE_READ_FAILED',
error instanceof Error ? error.message : 'Failed to read response body.',
)
}
if (response.ok) {
if (!text) {
return buildSyntheticErrorResult<TPath, TMethod, TExactStatuses>(
'default',
'EMPTY_SUCCESS_RESPONSE',
`${path} returned an empty response body.`,
)
}
try {
return {
ok: true,
data: JSON.parse(text) as JsonResponseFor<TPath, TMethod>,
}
} catch (error) {
return buildSyntheticErrorResult<TPath, TMethod, TExactStatuses>(
'default',
'INVALID_SUCCESS_RESPONSE',
error instanceof Error ? error.message : 'Server returned invalid JSON.',
)
}
}
if (!text) {
return buildSyntheticErrorResult<TPath, TMethod, TExactStatuses>(
resolveFallbackStatus(response.status),
`HTTP_${response.status}`,
`${response.status} ${response.statusText || 'Request failed'}`,
)
}
try {
const payload = JSON.parse(text) as unknown
return buildParsedErrorResult<TPath, TMethod, TExactStatuses>(
resolveErrorStatus(response.status, exactErrorStatuses),
payload,
)
} catch {
return buildSyntheticErrorResult<TPath, TMethod, TExactStatuses>(
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<TPath, TMethod>,
init?: RequestInit,
requestPath?: string,
): Promise<EndpointResult<TPath, TMethod, TExactStatuses>> => {
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<EndpointResult<'/api/models/download/progress', 'get'>> {
const path = `/api/models/download/progress${buildQueryString(query as Record<string, unknown>)}`
return requestEndpointResult('/api/models/download/progress', 'get', [] as const, undefined, path)
}
static listModels(
query?: QueryFor<'/api/models', 'get'>,
): Promise<EndpointResult<'/api/models', 'get'>> {
const path = `/api/models${buildQueryString(query as Record<string, unknown>)}`
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<EndpointResult<'/api/ic-loras/download/progress', 'get'>> {
const path = `/api/ic-loras/download/progress${buildQueryString(query as Record<string, unknown>)}`
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<EndpointResult<'/api/loras/download/progress', 'get'>> {
const path = `/api/loras/download/progress${buildQueryString(query as Record<string, unknown>)}`
return requestEndpointResult('/api/loras/download/progress', 'get', [] as const, undefined, path)
}
}
type ApiClientMethodName = keyof typeof ApiClient
export type ApiRequestBodyOf<TMethod extends ApiClientMethodName> = (typeof ApiClient)[TMethod] extends (
body?: infer TBody,
...args: any[]
) => Promise<any>
? TBody
: never
export type ApiSuccessOf<TMethod extends ApiClientMethodName> = (typeof ApiClient)[TMethod] extends (...args: any[]) => Promise<any>
? ApiSuccess<Awaited<ReturnType<(typeof ApiClient)[TMethod]>>>
: never
export type ApiErrorsOf<TMethod extends ApiClientMethodName> = (typeof ApiClient)[TMethod] extends (...args: any[]) => Promise<any>
? ApiErrors<Awaited<ReturnType<(typeof ApiClient)[TMethod]>>>
: never