import type { MagnificVideoModel, MagnificVideoFamily, MagnificCostTier } from './types.js'; const TIER_RANK: Record = { cheap: 0, standard: 1, premium: 2 }; /** * Conservative seed catalog (the 18 documented proxied models). Live discovery overrides. * * LIVE-BINDING NOTE (2026-06-16, api.magnific.com): video-gen models live at * `POST /v1/ai/image-to-video/` (NOT `/v1/ai/`), and each model has its OWN * request schema (e.g. Kling needs `{ image, duration }`). Confirmed: `kling-v2-6-pro` EXISTS; * `seedance-pro-1080p` returns 404 on this account (NOT exposed via the API — Seedance generation * stays on dreamina-useapi/seedance-direct). The other ids below are unverified against the live * catalog — the generation route needs a dedicated per-model path + request-schema binding pass * before it is end-to-end functional. (The video UPSCALE path/contract IS verified — see finish.ts.) */ export const MAGNIFIC_VIDEO_SEED_MODELS: MagnificVideoModel[] = [ // The image-to-video models VERIFIED LIVE against api.magnific.com (2026-06-16) — each accepts // POST /v1/ai/image-to-video/ and polls at the same path + /{task_id}. Per-model i2v field // differs (noted); the submit sends all variants so models ignore the key they don't use. // minimax-live is validated END-TO-END through the shipped transport (submit→poll→795KB video). // Stale/fictional slugs (seedance-pro-1080p [404], kling-v2-6-pro [submit-only, no poll], etc.) // were REMOVED — Magnific's API ≠ its web app catalog. text-to-video paths are unverified, so // these are i2v-only here until a t2v endpoint is confirmed. { id: 'minimax-live', family: 'hailuo', displayName: 'MiniMax Live', textToVideo: false, imageToVideo: true, resolution: '720p', costTier: 'cheap' }, // image_url + prompt { id: 'pixverse-v5', family: 'pixverse', displayName: 'PixVerse V5', textToVideo: false, imageToVideo: true, resolution: '1080p', costTier: 'cheap' }, // image_url + prompt + resolution { id: 'runway-gen4-turbo', family: 'runway', displayName: 'Runway Gen4 Turbo', textToVideo: false, imageToVideo: true, resolution: '1080p', costTier: 'standard' }, // image { id: 'kling-std', family: 'kling', displayName: 'Kling Standard', textToVideo: false, imageToVideo: true, resolution: '1080p', costTier: 'standard' }, // image { id: 'ltx-2-pro', family: 'ltx', displayName: 'LTX 2.0 Pro', textToVideo: false, imageToVideo: true, resolution: '1080p', costTier: 'standard' }, // image_url + prompt { id: 'kling-o1-pro', family: 'kling', displayName: 'Kling O1 Pro', textToVideo: false, imageToVideo: true, resolution: '1080p', costTier: 'premium' }, // first_frame ]; /** Cheapest model in a family (lowest cost tier; ties keep seed order). null if none. */ export function cheapestVideoModel( family: MagnificVideoFamily, models: readonly MagnificVideoModel[] = MAGNIFIC_VIDEO_SEED_MODELS, ): MagnificVideoModel | null { const inFamily = models.filter((m) => m.family === family); if (inFamily.length === 0) return null; return inFamily.reduce((best, m) => (TIER_RANK[m.costTier] < TIER_RANK[best.costTier] ? m : best)); } export function getVideoModel( id: string, models: readonly MagnificVideoModel[] = MAGNIFIC_VIDEO_SEED_MODELS, ): MagnificVideoModel { const m = models.find((x) => x.id === id); if (!m) throw new Error(`Unknown Magnific video model '${id}'. Known: ${models.map((x) => x.id).join(', ')}`); return m; } /** * Cheapest model in the WHOLE catalog that supports the requested operation. This is * the cheap-by-default selector for the magnific-rest route: it must NOT be pinned to * the seedance family, because Magnific's only Seedance model (seedance-pro-1080p) is * premium — pinning to seedance would make the default silently bill premium. A * 'text-to-video' request requires textToVideo; 'image-to-video' requires imageToVideo; * any other operation imposes no capability filter. Ties keep seed order. null if none. */ export function cheapestModelForOperation( operation: string, models: readonly MagnificVideoModel[] = MAGNIFIC_VIDEO_SEED_MODELS, ): MagnificVideoModel | null { const capable = models.filter((m) => { if (operation === 'text-to-video') return m.textToVideo; if (operation === 'image-to-video') return m.imageToVideo; return true; }); if (capable.length === 0) return null; return capable.reduce((best, m) => (TIER_RANK[m.costTier] < TIER_RANK[best.costTier] ? m : best)); } interface MinimalResponse { ok: boolean; status: number; json: () => Promise; text: () => Promise; } type ModelsFetchLike = (url: string, init?: { headers?: Record }) => Promise; /** * Live model discovery. Queries Magnific's model list; on ANY error returns the seed * table verbatim (so tests/dry-runs work offline). The mapping of the live response to * MagnificVideoModel[] is bound during execution against the real api-reference. */ export async function listMagnificVideoModels( env: NodeJS.ProcessEnv = process.env, opts: { fetchImpl?: ModelsFetchLike } = {}, ): Promise { const key = (env.MAGNIFIC_API_KEY ?? '').trim(); const fetchImpl = opts.fetchImpl; if (!key || !fetchImpl) return MAGNIFIC_VIDEO_SEED_MODELS; try { const base = (env.VCLAW_MAGNIFIC_API_URL ?? 'https://api.magnific.com').trim(); const res = await fetchImpl(`${base}/v1/ai/video-models`, { headers: { 'x-magnific-api-key': key } }); if (!res.ok) return MAGNIFIC_VIDEO_SEED_MODELS; const body = await res.json(); const mapped = mapLiveModels(body); return mapped.length > 0 ? mapped : MAGNIFIC_VIDEO_SEED_MODELS; } catch { return MAGNIFIC_VIDEO_SEED_MODELS; } } /** Best-effort mapper; tolerant of shape. Returns [] when it can't recognize the payload. */ function mapLiveModels(body: unknown): MagnificVideoModel[] { const rows: unknown[] = Array.isArray(body) ? body : Array.isArray((body as { data?: unknown[] })?.data) ? ((body as { data: unknown[] }).data) : []; const out: MagnificVideoModel[] = []; for (const r of rows) { const id = (r as { id?: unknown })?.id; const seed = MAGNIFIC_VIDEO_SEED_MODELS.find((s) => s.id === id); if (seed) out.push(seed); // keep curated metadata; presence in live list = available } return out; }