/** * provider-wire-preview.ts — the EXACT request a route's transport will send * for each scene, computed from the same payload the submit uses, so the * reviewed contract and the submitted one are the same object. * * WHY: working-discipline rule 5 says the review portal IS the contract and * must show the exact submit payload, not a paraphrase. The run contract froze * the prompt, the references and the profile, but the WIRE body — what the * vendor parses — was re-derived by hand in the portal, and for * `seedance-modelark` that hand-written body was the xskill shape the route * never sends. This module asks the transport's own planner instead. * * Route coverage is explicit: a route without a planner here yields nothing, * and the portal keeps whatever it rendered before. Never throws — a scene the * planner refuses is recorded as `refused` with the planner's own words, which * is exactly what the submit would say, shown before any spend. Only the * PLANNER runs here: the reference-file checks (missing file, vendor size caps, * reference seconds) and the content-filter warnings happen at submit and are * not previewed, so a body here is not a promise the submit will be accepted. */ import { planModelArkTask, buildModelArkCreateBody, modelArkBaseUrl, modelArkChainOptions, resolveModelArkModel, MODELARK_TASKS_PATH } from './providers/modelark.js'; import { loadModelArkWorkspaceEnv } from './native-modelark.js'; import type { VideoExecutionPayload } from './types.js'; export type ProviderWirePreview = | { /** The `ActiveTransport` name that would send it. */ transport: string; /** Where the body would be POSTed. */ endpoint: string; /** The body as the vendor parses it. Local references appear as their * SOURCE path: at submit they are inlined or hosted, and that URL is not * knowable before the upload. */ body: unknown; } | { transport: string; /** The transport's own refusal, verbatim — what the submit would say. */ refused: string; }; /** * sceneIndex → the wire preview, for routes that expose their planner. Reads * `.env.local` under the process environment, as the transport does, through * the route's own resolver; an unknown model or a missing key is reported per * scene as a refusal rather than thrown. */ export async function previewProviderWire( payload: VideoExecutionPayload, env: NodeJS.ProcessEnv = process.env, ): Promise> { const out = new Map(); if (payload.routeId !== 'seedance-modelark') return out; const transport = 'native-modelark'; // The transport's own env merge (.env.local under the process env), used for // BOTH the model and the base URL: the endpoint decides which account is // billed, so a preview built from a different env than the submit would // name the wrong host. const merged = await loadModelArkWorkspaceEnv(payload.workspaceRoot, env); let model: ReturnType; const chainOptions = modelArkChainOptions(merged); try { model = resolveModelArkModel(merged); } catch (error) { for (const task of payload.tasks) out.set(task.sceneIndex, { transport, refused: error instanceof Error ? error.message : String(error) }); return out; } const endpoint = `${modelArkBaseUrl(merged)}${MODELARK_TASKS_PATH}`; for (const task of payload.tasks) { try { const plan = planModelArkTask(task, payload.executionProfile, model, chainOptions); out.set(task.sceneIndex, { transport, endpoint, body: buildModelArkCreateBody(plan, plan.refs.map((ref) => ref.source)) }); } catch (error) { out.set(task.sceneIndex, { transport, refused: error instanceof Error ? error.message : String(error) }); } } return out; }