/** * flow-resolution.ts — shared output-resolution resolution for the in-repo Google * Flow (useapi.net) `POST /videos` submit paths, and the companion upscale target. * * Google's 2026-08-26 Omni 1.1 Flash update added a `resolution` parameter to * POST /videos: `360p` or `720p`, default `720p`, **`omni-flash` only**. Veo * publishes no 360p variant, so the API REJECTS `360p` on a Veo model rather than * silently generating 720p at full price — which is why this module refuses the * pairing locally instead of letting a paid submit bounce. * * Why 360p is worth wiring: a 360p generation costs roughly half its 720p * equivalent (4s: 4 credits vs 7; a V2V edit: 10 vs 20) and renders faster * (~35s vs ~45s for the same 4s clip). It is the tier to ITERATE on wording in. * It is NOT the tier to deliver from — useapi's own launch post is explicit that * "an upscaled 360p clip is not the same picture as one generated at 720p", so * the draft loop runs at 360p and the take that matters is generated at 720p. * Either one finishes at 1080p through the free upscale (see * {@link ../providers/google-flow.js}). * * Spec: https://useapi.net/docs/api-google-flow-v1/post-google-flow-videos * (LLM text form: https://useapi.net/assets/aibot/api-google-flow-v1.txt). * * Shaped deliberately like {@link ./flow-captcha.js}: an env-read that returns * `undefined` when unset, so a body with no resolution is BYTE-IDENTICAL to what * we sent before this parameter existed. */ /** Output resolutions `POST /videos` accepts. `omni-flash` only. */ export const FLOW_VIDEO_RESOLUTIONS = ['360p', '720p'] as const; export type FlowVideoResolution = (typeof FLOW_VIDEO_RESOLUTIONS)[number]; /** The API's own default when the field is omitted. We omit rather than send it. */ export const FLOW_VIDEO_RESOLUTION_DEFAULT: FlowVideoResolution = '720p'; /** The only model family that accepts `resolution` at all. */ export const FLOW_RESOLUTION_MODELS = ['omni-flash', 'omni'] as const; export function isFlowVideoResolution(value: unknown): value is FlowVideoResolution { return typeof value === 'string' && (FLOW_VIDEO_RESOLUTIONS as readonly string[]).includes(value); } /** * Read the Flow output resolution from the environment (`VCLAW_FLOW_RESOLUTION`). * Returns `undefined` when unset or blank — the caller then omits the field and * the submit body is unchanged from the pre-2026-08-26 shape. Throws on a value * that is set but not one of the two the API accepts, rather than silently * dropping it: a typo'd `1080p` here would otherwise cost a full-price render * while the operator believed they were drafting. */ export function resolveFlowVideoResolution( env: NodeJS.ProcessEnv = process.env, ): FlowVideoResolution | undefined { const raw = (env.VCLAW_FLOW_RESOLUTION ?? '').trim(); if (raw === '') return undefined; const lower = raw.toLowerCase(); if (!isFlowVideoResolution(lower)) { throw new Error( `VCLAW_FLOW_RESOLUTION must be one of ${FLOW_VIDEO_RESOLUTIONS.join('|')}, got ${JSON.stringify(raw)}. ` + `(1080p is an UPSCALE target, not a generation resolution — see vclaw video finish --backend google-flow.)`, ); } return lower; } /** * Fail fast when a resolution is paired with a model that cannot take one. Veo * has no 360p variant and the API rejects the pairing, so catching it here turns * a wasted round-trip into a clear local error. An `undefined` resolution is * always fine — that is the omit-the-field path. */ export function assertFlowResolution(model: string | undefined, resolution: FlowVideoResolution | undefined): void { if (resolution === undefined) return; const normalized = (model ?? '').trim().toLowerCase(); if (!(FLOW_RESOLUTION_MODELS as readonly string[]).includes(normalized)) { throw new Error( `Google Flow: resolution "${resolution}" is omni-flash only (got model "${model ?? '(default)'}"). ` + `Veo publishes no 360p variant and the API rejects the pairing.`, ); } } /** * Set `resolution` on a Flow submit body when one is requested (mutates and * returns the body for chaining). `undefined` leaves the body untouched, which * is what keeps legacy payloads byte-identical. */ export function applyFlowResolution>( body: T, resolution: FlowVideoResolution | undefined, ): T { if (resolution !== undefined) (body as Record).resolution = resolution; return body; }