import { flattenHeaders, getContext, getTestId, isErrorResponse, isVideoResponse, resolveResponse, resolveStrictMode, serializeErrorResponse, strictNoMatchLogLine, strictNoMatchMessage, strictOverrideField } from "./helpers.js";
import { matchFixtureDiagnostic } from "./router.js";
import { writeErrorResponse } from "./sse-writer.js";
import { applyChaos } from "./chaos.js";
import { resolveUpstreamUrl } from "./url.js";
import { buildFixtureMatch, buildForwardHeaders, clampTimeout, persistFixture, sanitizeHeaderValue } from "./recorder.js";
import { resolveProgression } from "./fal.js";
import { DEFAULT_UPSTREAM_TIMEOUT_MS, readBodyIdle, readEnvelopeText, requestBase, testIdSuffix, upstreamTimeoutSignal } from "./video-proxy-shared.js";
import crypto from "node:crypto";

//#region src/openrouter-video.ts
const DEFAULT_OPENROUTER_VIDEO_MODEL = "bytedance/seedance-2.0";
const OPENROUTER_VIDEO_MAX_ENTRIES = 1e4;
const OPENROUTER_VIDEO_TTL_MS = 36e5;
/**
* Per-testId job state for the OpenRouter video handler. Mirrors
* FalQueueStateMap (fal.ts): lazy TTL eviction on `get`, FIFO eviction of the
* oldest entries on `set` when over capacity, no background sweep timer.
* Deliberate divergences from FalQueueStateMap:
*   - lifetime: this map is per-server-instance (cleared on server
*     close/reset) while FalQueueStateMap is module-global;
*   - `set()` refreshes: delete-before-set moves a re-inserted key to the
*     back of the FIFO order AND resets its createdAt, so a refreshed entry
*     gets a fresh TTL — FalQueueStateMap's plain overwrite keeps the
*     original insertion slot and is never used as a TTL refresh.
* Keys are `${testId}:${jobId}`.
*/
var OpenRouterVideoJobMap = class {
	entries = /* @__PURE__ */ new Map();
	worldGeneration = 0;
	/**
	* Monotonic world-generation counter, incremented by `clear()` (a fixtures
	* reset). Identity checks (`jobs.get(key) === job`) can only guard
	* continuations whose job was ALREADY inserted — a continuation that has
	* not inserted yet (the submit proxy's upstream fetch) captures this value
	* before its await and compares after, so it can detect that the world it
	* belongs to was reset mid-flight and skip seeding the new world.
	*/
	get generation() {
		return this.worldGeneration;
	}
	get(key) {
		const entry = this.entries.get(key);
		if (!entry) return void 0;
		if (Date.now() - entry.createdAt > OPENROUTER_VIDEO_TTL_MS) {
			this.entries.delete(key);
			return;
		}
		return entry.job;
	}
	set(key, job) {
		this.entries.delete(key);
		this.entries.set(key, {
			job,
			createdAt: Date.now()
		});
		if (this.entries.size > OPENROUTER_VIDEO_MAX_ENTRIES) {
			const excess = this.entries.size - OPENROUTER_VIDEO_MAX_ENTRIES;
			const iter = this.entries.keys();
			for (let i = 0; i < excess; i++) {
				const next = iter.next();
				if (!next.done) this.entries.delete(next.value);
			}
		}
	}
	delete(key) {
		return this.entries.delete(key);
	}
	clear() {
		this.entries.clear();
		this.worldGeneration++;
	}
	get size() {
		return this.entries.size;
	}
};
/**
* Maps the fixture's terminal video status onto the job lifecycle. Anything
* that is not "failed" — including a fixture authored as "processing" — is
* treated as completed, since this surface always drives jobs to a terminal
* state. handleOpenRouterVideoCreate warns when a "processing" fixture is
* coerced this way.
*/
function terminalStatus(job) {
	return job.video.status === "failed" ? "failed" : "completed";
}
/**
* Mutates a job in place to advance its state on a status poll.
* `pending → in_progress → completed | failed` based on poll-count thresholds.
* No-op once terminal. The in_progress threshold is checked first so a job
* whose thresholds are equal still spends one poll in in_progress instead of
* jumping straight to the terminal status (fal advanceJob semantics).
*/
function advanceJob(job) {
	if (job.status === "completed" || job.status === "failed") return;
	job.pollCount += 1;
	if (job.status === "pending" && job.pollCount >= job.pollsBeforeInProgress) job.status = "in_progress";
	else if (job.pollCount >= job.pollsBeforeCompleted) job.status = terminalStatus(job);
}
/**
* Fetch a byte-bearing upstream URL with `record.upstreamTimeoutMs` gating
* only the HEADERS: the abort timer is cleared the moment the response head
* arrives, so a long — but steadily progressing — body download is never
* killed by a total deadline. Body progress is governed separately by
* `readBodyIdle` (between-chunk idle semantics).
*/
async function fetchHeadersWithTimeout(url, headers, record) {
	const timeoutMs = clampTimeout(record?.upstreamTimeoutMs, DEFAULT_UPSTREAM_TIMEOUT_MS);
	const controller = new AbortController();
	const timer = setTimeout(() => controller.abort(/* @__PURE__ */ new Error(`Upstream response headers timed out after ${timeoutMs}ms`)), timeoutMs);
	try {
		return await fetch(url, {
			headers,
			signal: controller.signal
		});
	} finally {
		clearTimeout(timer);
	}
}
/**
* Cap on an upstream ERROR body (401/403 relays) buffered by the content
* proxy. Error bodies are envelope-sized JSON in practice — 64 KB is generous
* — and unlike the video bytes they are buffered in full before the relay, so
* an unbounded read would be a memory hole on a hostile upstream.
*/
const OPENROUTER_VIDEO_ERROR_BODY_CAP = 64 * 1024;
/**
* Rewrite an upstream poll body for relay to the client. Every field is
* relayed verbatim EXCEPT the ones that would let the client escape the mock
* or learn upstream identifiers:
*   - `id` → the mock jobId;
*   - a present `polling_url` → the mock's poll URL (testId embedded);
*   - a present `unsigned_urls` array → an array of the SAME length whose
*     element i is the mock content URL with `?index=i`. On post-capture
*     REPLAYS the mock content endpoint serves the index-0 bytes for every
*     index (only index 0 is captured); during the capture window and under
*     proxyOnly the index selects the live-proxied upstream URL.
* `usage` passes through untouched: a non-number `usage.cost` warns but is
* never coerced or dropped, and no usage is ever invented.
*/
function rewriteRecordPollBody(args) {
	const { upstreamBody, job, req, testId, logger } = args;
	const base = requestBase(req, logger);
	const body = {
		...upstreamBody,
		id: job.jobId
	};
	if (upstreamBody.polling_url !== void 0) body.polling_url = `${base}/api/v1/videos/${job.jobId}${testIdSuffix(testId, "?")}`;
	if (Array.isArray(upstreamBody.unsigned_urls)) body.unsigned_urls = upstreamBody.unsigned_urls.map((_url, i) => `${base}/api/v1/videos/${job.jobId}/content?index=${i}${testIdSuffix(testId, "&")}`);
	else if (upstreamBody.unsigned_urls !== void 0) {
		delete body.unsigned_urls;
		if (!job.nonArrayUrlsWarned) {
			job.nonArrayUrlsWarned = true;
			logger.warn(`Upstream video job ${job.upstreamJobId} reported a non-array unsigned_urls (${JSON.stringify(upstreamBody.unsigned_urls).slice(0, 100)}) — stripped from the relay`);
		}
	}
	const usage = upstreamBody.usage;
	if (usage !== null && typeof usage === "object" && !Array.isArray(usage)) {
		const cost = usage.cost;
		if (cost !== void 0 && typeof cost !== "number" && !job.badCostWarned) {
			job.badCostWarned = true;
			logger.warn(`Upstream video job ${job.upstreamJobId} reported a non-number usage.cost (${JSON.stringify(cost).slice(0, 50)}) — passing it through untouched`);
		}
	}
	return body;
}
/**
* Synthesizes a structurally valid journal body for field-validation 400s.
* JournalEntry.body is typed `ChatCompletionRequest | null`, so the raw
* parsed body cannot be journaled as-is — journal consumers may walk
* `body.messages`. The result is ChatCompletionRequest-shaped: `model` is a
* string (a non-string value is JSON-encoded so the field stays a string
* without dropping what the caller sent) and `messages` is an empty array
* (there is no validated prompt to wrap). Raw request fields (including
* `prompt`) are preserved via the index signature only on this validation
* path — the success path's syntheticReq is built from scratch and does not
* carry them.
*/
function validationJournalBody(videoReq) {
	const rawModel = videoReq.model;
	const model = typeof rawModel === "string" ? rawModel : rawModel === void 0 ? "" : JSON.stringify(rawModel);
	return {
		...Object.fromEntries(Object.entries(videoReq).filter(([key]) => !key.startsWith("_"))),
		model,
		messages: []
	};
}
async function handleOpenRouterVideoStatus(req, res, jobId, fixtures, journal, defaults, setCorsHeaders, jobs) {
	setCorsHeaders(res);
	const path = req.url ?? `/api/v1/videos/${jobId}`;
	const method = req.method ?? "GET";
	if (applyChaos(res, null, defaults.chaos, req.headers, journal, {
		method,
		path,
		headers: flattenHeaders(req.headers),
		body: null
	}, "internal", defaults.registry, defaults.logger)) return;
	const testId = getTestId(req);
	const job = jobs.get(`${testId}:${jobId}`);
	if (!job) {
		journal.add({
			method,
			path,
			headers: flattenHeaders(req.headers),
			body: null,
			response: {
				status: 404,
				fixture: null
			}
		});
		writeErrorResponse(res, 404, JSON.stringify({ error: {
			message: `Video job ${jobId} not found`,
			code: 404
		} }));
		return;
	}
	if (job.kind === "record") {
		if (resolveStrictMode(defaults.strict, req.headers)) {
			defaults.logger.error(`STRICT: video job ${jobId} is proxied live upstream (record mode) — refusing the upstream poll`);
			journal.add({
				method,
				path,
				headers: flattenHeaders(req.headers),
				body: null,
				response: {
					status: 503,
					fixture: null,
					...strictOverrideField(defaults.strict, req.headers)
				}
			});
			writeErrorResponse(res, 503, JSON.stringify({ error: {
				message: `Strict mode: video job ${jobId} is proxied live upstream (record mode) — nothing reaches an upstream under strict mode`,
				type: "invalid_request_error",
				code: "no_fixture_match"
			} }));
			return;
		}
		await proxyOpenRouterVideoRecordPoll({
			req,
			res,
			job,
			key: `${testId}:${jobId}`,
			testId,
			fixtures,
			journal,
			defaults,
			jobs,
			method,
			path
		});
		return;
	}
	if (res.destroyed || res.writableEnded) return;
	advanceJob(job);
	jobs.set(`${testId}:${jobId}`, job);
	journal.add({
		method,
		path,
		headers: flattenHeaders(req.headers),
		body: null,
		response: {
			status: 200,
			fixture: null
		}
	});
	const body = {
		id: job.jobId,
		status: job.status
	};
	if (job.status === "completed") {
		body.unsigned_urls = [`${requestBase(req, defaults.logger)}/api/v1/videos/${job.jobId}/content?index=0${testIdSuffix(testId, "&")}`];
		body.usage = { cost: job.video.cost ?? 0 };
	} else if (job.status === "failed") {
		if (job.video.error === "" && !job.emptyErrorWarned) {
			job.emptyErrorWarned = true;
			defaults.logger.warn(`Video fixture for job ${job.jobId} has an empty error message — using the default`);
		}
		body.error = job.video.error || "Video generation failed";
	}
	res.writeHead(200, { "Content-Type": "application/json" });
	res.end(JSON.stringify(body));
}
const PLACEHOLDER_MP4 = Buffer.from([
	0,
	0,
	0,
	24,
	102,
	116,
	121,
	112,
	105,
	115,
	111,
	109,
	0,
	0,
	2,
	0,
	105,
	115,
	111,
	109,
	109,
	112,
	52,
	50
]);
async function handleOpenRouterVideoContent(req, res, jobId, journal, defaults, setCorsHeaders, jobs) {
	setCorsHeaders(res);
	const path = req.url ?? `/api/v1/videos/${jobId}/content`;
	const method = req.method ?? "GET";
	if (applyChaos(res, null, defaults.chaos, req.headers, journal, {
		method,
		path,
		headers: flattenHeaders(req.headers),
		body: null
	}, "internal", defaults.registry, defaults.logger)) return;
	const authorization = req.headers.authorization;
	if (!authorization || !/^bearer\s+\S/i.test(authorization)) {
		journal.add({
			method,
			path,
			headers: flattenHeaders(req.headers),
			body: null,
			response: {
				status: 401,
				fixture: null
			}
		});
		writeErrorResponse(res, 401, JSON.stringify({ error: {
			message: "No auth credentials found",
			code: 401
		} }));
		return;
	}
	const testId = getTestId(req);
	const job = jobs.get(`${testId}:${jobId}`);
	if (!job) {
		journal.add({
			method,
			path,
			headers: flattenHeaders(req.headers),
			body: null,
			response: {
				status: 404,
				fixture: null
			}
		});
		writeErrorResponse(res, 404, JSON.stringify({ error: {
			message: `Video job ${jobId} not found`,
			code: 404
		} }));
		return;
	}
	if (job.status !== "completed") {
		journal.add({
			method,
			path,
			headers: flattenHeaders(req.headers),
			body: null,
			response: {
				status: 400,
				fixture: null
			}
		});
		writeErrorResponse(res, 400, JSON.stringify({ error: {
			message: `Video job ${jobId} is not completed (status: ${job.status})`,
			code: 400
		} }));
		return;
	}
	const queryIdx = path.indexOf("?");
	const indexParam = queryIdx === -1 ? null : new URLSearchParams(path.slice(queryIdx + 1)).get("index");
	if (job.kind === "record") {
		if (resolveStrictMode(defaults.strict, req.headers)) {
			defaults.logger.error(`STRICT: video job ${jobId} content is proxied live upstream (record mode) — refusing the upstream fetch`);
			journal.add({
				method,
				path,
				headers: flattenHeaders(req.headers),
				body: null,
				response: {
					status: 503,
					fixture: null,
					...strictOverrideField(defaults.strict, req.headers)
				}
			});
			writeErrorResponse(res, 503, JSON.stringify({ error: {
				message: `Strict mode: video job ${jobId} is proxied live upstream (record mode) — nothing reaches an upstream under strict mode`,
				type: "invalid_request_error",
				code: "no_fixture_match"
			} }));
			return;
		}
		await proxyOpenRouterVideoRecordContent({
			req,
			res,
			job,
			key: `${testId}:${jobId}`,
			indexParam,
			journal,
			defaults,
			jobs,
			method,
			path
		});
		return;
	}
	if (indexParam !== null && Number(indexParam) !== 0) defaults.logger.warn(`Video content request for job ${jobId} asked for index=${indexParam} — the index param is ignored (jobs are single-video); serving index 0`);
	let bytes;
	if (job.video.b64) {
		bytes = Buffer.from(job.video.b64, "base64");
		const sanitized = job.video.b64.replace(/\s+/g, "").replace(/-/g, "+").replace(/_/g, "/").replace(/=.*$/, "");
		const expectedBytes = Math.floor(sanitized.length * 3 / 4);
		if (bytes.length === 0 && !job.contentWarnsLatched) defaults.logger.warn(`Video fixture b64 for job ${jobId} decoded to zero bytes — the fixture is not controlling the served content`);
		if (sanitized.length % 4 === 1) {
			if (!job.contentWarnsLatched) defaults.logger.warn(`Video fixture b64 for job ${jobId} has length ≡ 1 (mod 4) after sanitization (${sanitized.length} chars) — payload is malformed or contains invalid characters`);
		} else if (bytes.length !== expectedBytes && !job.contentWarnsLatched) defaults.logger.warn(`Video fixture b64 for job ${jobId} decoded to ${bytes.length} bytes where its length implies ${expectedBytes} — likely corrupt base64`);
	} else {
		if (job.video.b64 === "" && !job.contentWarnsLatched) defaults.logger.warn(`Video fixture for job ${jobId} has an empty b64 — serving the placeholder MP4`);
		if (job.video.url && !job.contentWarnsLatched) defaults.logger.warn(`Video fixture for job ${jobId} sets video.url but no b64 — url is ignored on the OpenRouter content endpoint; use b64 to control the served bytes (serving the placeholder MP4)`);
		bytes = PLACEHOLDER_MP4;
	}
	job.contentWarnsLatched = true;
	if (res.destroyed || res.writableEnded) return;
	jobs.set(`${testId}:${jobId}`, job);
	journal.add({
		method,
		path,
		headers: flattenHeaders(req.headers),
		body: null,
		response: {
			status: 200,
			fixture: null
		}
	});
	res.writeHead(200, {
		"Content-Type": "video/mp4",
		"Content-Length": bytes.length
	});
	res.end(bytes);
}
/**
* Live-proxy a content download for a completed record job — reachable under
* proxyOnly (forever) and during the eager-capture window: the stored
* upstream `unsigned_urls[index || 0]` is fetched with the same same-origin
* Bearer-forwarding gate as the eager-capture path (headers gated by
* `upstreamTimeoutMs`, body by `bodyTimeoutMs` idle semantics), and the bytes
* are STREAMED to the client as `video/mp4` (matching the real API regardless
* of the upstream's content-type) — chunks are relayed as they arrive, so the
* whole video is never buffered in memory. An upstream 401/403 passes through
* verbatim (real-API fidelity); other pre-relay failures 502 (a mid-stream
* stall can only abort the already-committed 200). No state is mutated and
* nothing is cached — repeated downloads hit the upstream every time.
* Journaled source:"proxy" when the relay commits. Refuses with 502 before
* contacting the upstream when record mode was disabled mid-flight. A
* committed relay refreshes the job's TTL (identity-guarded) — a client
* working through a long download list must not lose the job mid-way.
*/
async function proxyOpenRouterVideoRecordContent(args) {
	const { req, res, job, key, indexParam, journal, defaults, jobs, method, path } = args;
	const logger = defaults.logger;
	const proxyError = (msg) => {
		logger.error(`OpenRouter video content proxy failed: ${msg}`);
		if (res.destroyed || res.writableEnded) return;
		journal.add({
			method,
			path,
			headers: flattenHeaders(req.headers),
			body: null,
			response: {
				status: 502,
				fixture: null,
				source: "proxy",
				...strictOverrideField(defaults.strict, req.headers)
			}
		});
		writeErrorResponse(res, 502, JSON.stringify({ error: {
			message: `Proxy to upstream failed: ${msg}`,
			type: "proxy_error"
		} }));
	};
	const record = defaults.record;
	if (!record) {
		proxyError("record mode is no longer configured for an in-flight record job");
		return;
	}
	const urls = job.upstreamUnsignedUrls ?? [];
	const parsedIndex = indexParam === null ? 0 : Number(indexParam);
	const index = Number.isInteger(parsedIndex) && parsedIndex >= 0 ? parsedIndex : 0;
	if (indexParam !== null && index !== parsedIndex) logger.warn(`Video content request for job ${job.jobId} has an unusable index=${indexParam} — serving index 0`);
	const addressed = urls[index];
	let target = typeof addressed === "string" && addressed ? addressed : void 0;
	if (target === void 0 && index !== 0) {
		if (index >= urls.length) logger.warn(`Video content request for job ${job.jobId} asked for index=${index} but the upstream reported only ${urls.length} unsigned_urls — serving index 0`);
		else logger.warn(`Video content request for job ${job.jobId} asked for index=${index} but the upstream's unsigned_urls[${index}] is unusable (non-string or empty) — serving index 0`);
		const first = urls[0];
		target = typeof first === "string" && first ? first : void 0;
	}
	if (!target) {
		proxyError(urls.length === 0 ? `Upstream job ${job.upstreamJobId} completed without usable unsigned_urls` : `Upstream job ${job.upstreamJobId} completed but its unsigned_urls[0] is unusable (non-string or empty)`);
		return;
	}
	let contentUrl;
	try {
		contentUrl = new URL(target);
	} catch {
		proxyError(`Upstream unsigned_urls[${index}] is not a valid URL (${target.slice(0, 100)})`);
		return;
	}
	const providerBase = record.providers.openrouter;
	let providerOrigin;
	try {
		providerOrigin = new URL(providerBase ?? job.upstreamPollingUrl).origin;
	} catch {
		try {
			providerOrigin = new URL(job.upstreamPollingUrl).origin;
		} catch {
			proxyError(`Cannot determine the provider origin (invalid provider URL and polling URL) — refusing to forward credentials`);
			return;
		}
	}
	const headers = buildForwardHeaders(req);
	if (contentUrl.origin !== providerOrigin) {
		delete headers.authorization;
		logger.warn(`Upstream unsigned_urls[${index}] origin ${contentUrl.origin} differs from the provider origin ${providerOrigin} — fetching content WITHOUT the client's Authorization header`);
	}
	let contentRes;
	try {
		contentRes = await fetchHeadersWithTimeout(contentUrl, headers, record);
	} catch (err) {
		proxyError(err instanceof Error ? err.message : "Unknown proxy error");
		return;
	}
	if (contentRes.status === 401 || contentRes.status === 403) {
		let errBody;
		try {
			const read = await readBodyIdle(contentRes, record, OPENROUTER_VIDEO_ERROR_BODY_CAP);
			if (read.overCap) {
				logger.warn(`Upstream ${contentRes.status} error body for job ${job.upstreamJobId} exceeded ${OPENROUTER_VIDEO_ERROR_BODY_CAP} bytes — relaying the status with an empty body`);
				errBody = Buffer.alloc(0);
			} else errBody = read.buf;
		} catch (err) {
			proxyError(err instanceof Error ? err.message : "Unknown proxy error");
			return;
		}
		logger.warn(`Upstream content host rejected the download for job ${job.upstreamJobId} (${contentRes.status}) — relaying the upstream status`);
		if (res.destroyed || res.writableEnded) return;
		journal.add({
			method,
			path,
			headers: flattenHeaders(req.headers),
			body: null,
			response: {
				status: contentRes.status,
				fixture: null,
				source: "proxy",
				...strictOverrideField(defaults.strict, req.headers)
			}
		});
		res.writeHead(contentRes.status, { "Content-Type": contentRes.headers.get("content-type") ?? "application/json" });
		res.end(errBody);
		return;
	}
	if (!contentRes.ok) {
		let sample = "";
		try {
			const read = await readBodyIdle(contentRes, record, OPENROUTER_VIDEO_ERROR_BODY_CAP);
			if (!read.overCap) sample = read.buf.toString("utf8").slice(0, 200);
		} catch {}
		proxyError(`Content ${contentRes.status}${sample ? `: ${sample}` : ""}`);
		return;
	}
	if (res.destroyed || res.writableEnded) {
		contentRes.body?.cancel().catch(() => {});
		return;
	}
	journal.add({
		method,
		path,
		headers: flattenHeaders(req.headers),
		body: null,
		response: {
			status: 200,
			fixture: null,
			source: "proxy",
			...strictOverrideField(defaults.strict, req.headers)
		}
	});
	if (jobs.get(key) === job) jobs.set(key, job);
	res.writeHead(200, { "Content-Type": "video/mp4" });
	const body = contentRes.body;
	if (!body) {
		res.end();
		return;
	}
	const idleMs = clampTimeout(record.bodyTimeoutMs, DEFAULT_UPSTREAM_TIMEOUT_MS);
	const reader = body.getReader();
	try {
		for (;;) {
			let idleTimer;
			const readPromise = reader.read();
			readPromise.catch(() => {});
			const result = await Promise.race([readPromise, new Promise((_, reject) => {
				idleTimer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Upstream response body idle for ${idleMs}ms`)), idleMs);
			})]).finally(() => clearTimeout(idleTimer));
			if (result.done) break;
			if (res.destroyed) return;
			if (!res.write(Buffer.from(result.value))) {
				if (!await new Promise((resolve) => {
					const cleanup = () => {
						res.off("drain", onDrain);
						res.off("close", onClose);
						clearTimeout(drainTimer);
					};
					const onDrain = () => {
						cleanup();
						resolve(true);
					};
					const onClose = () => {
						cleanup();
						resolve(true);
					};
					const drainTimer = setTimeout(() => {
						cleanup();
						resolve(false);
					}, idleMs);
					res.once("drain", onDrain);
					res.once("close", onClose);
				})) {
					logger.warn(`OpenRouter video content relay for job ${job.upstreamJobId} aborted: the client stopped reading for ${idleMs}ms — destroying the response and releasing the upstream`);
					res.destroy();
					return;
				}
			}
		}
		res.end();
	} catch (err) {
		logger.error(`OpenRouter video content relay aborted mid-stream: ${err instanceof Error ? err.message : String(err)}`);
		res.destroy();
	} finally {
		reader.cancel().catch(() => {});
	}
}
const DEFAULT_OPENROUTER_VIDEO_MODELS = [DEFAULT_OPENROUTER_VIDEO_MODEL, "openai/sora-2"];
function modelEntry(id) {
	return {
		id,
		name: id,
		supported_durations: [4, 8],
		supported_resolutions: ["720p", "1080p"],
		supported_aspect_ratios: [
			"16:9",
			"9:16",
			"1:1"
		],
		supported_frame_images: [],
		supported_sizes: [],
		generate_audio: false,
		seed: true,
		pricing_skus: []
	};
}
/**
* Synthesizes the OpenRouter video model listing from loaded fixtures —
* video-endpoint fixtures with a string `match.model` (mirrors the Ollama
* `/api/tags` synthesis in server.ts). Falls back to a default model set when
* no video fixtures are loaded. Note video models do not appear in the plain
* `/api/v1/models` listing on the real API, hence the dedicated route.
*
* With `record.providers.openrouter` configured the listing is proxied
* upstream instead and relayed verbatim on success (journaled
* source:"proxy", never recorded as a fixture); an upstream failure warns
* and falls back to the synthesis below. Strict mode disables the proxy —
* a strict request is always served the synthesized listing (strict means
* "nothing reaches an upstream").
*/
async function handleOpenRouterVideoModels(req, res, fixtures, journal, defaults, setCorsHeaders) {
	setCorsHeaders(res);
	const path = req.url ?? "/api/v1/videos/models";
	const method = req.method ?? "GET";
	if (applyChaos(res, null, defaults.chaos, req.headers, journal, {
		method,
		path,
		headers: flattenHeaders(req.headers),
		body: null
	}, "internal", defaults.registry, defaults.logger)) return;
	const record = defaults.record;
	const upstreamBase = record?.providers.openrouter;
	let proxyAttemptFailed = false;
	if (upstreamBase && !resolveStrictMode(defaults.strict, req.headers)) {
		let relay;
		try {
			const target = resolveUpstreamUrl(upstreamBase, "/api/v1/videos/models");
			const upstreamRes = await fetch(target, {
				headers: buildForwardHeaders(req),
				signal: upstreamTimeoutSignal(record)
			});
			const text = await readEnvelopeText(upstreamRes, record);
			if (!upstreamRes.ok) throw new Error(`Models ${upstreamRes.status}: ${text.slice(0, 200)}`);
			relay = {
				text,
				contentType: upstreamRes.headers.get("content-type") ?? "application/json"
			};
		} catch (err) {
			proxyAttemptFailed = true;
			const msg = err instanceof Error ? err.message : "Unknown proxy error";
			defaults.logger.warn(`OpenRouter video models proxy failed (${msg}) — falling back to the synthesized listing`);
		}
		if (relay) {
			if (res.destroyed || res.writableEnded) return;
			journal.add({
				method,
				path,
				headers: flattenHeaders(req.headers),
				body: null,
				response: {
					status: 200,
					fixture: null,
					source: "proxy",
					...strictOverrideField(defaults.strict, req.headers)
				}
			});
			res.writeHead(200, { "Content-Type": relay.contentType });
			res.end(relay.text);
			return;
		}
	}
	const modelIds = /* @__PURE__ */ new Set();
	let sawVideoFixture = false;
	for (const f of fixtures) if (f.match.endpoint === "video") {
		sawVideoFixture = true;
		if (f.match.model && typeof f.match.model === "string") modelIds.add(f.match.model);
	}
	if (modelIds.size === 0 && sawVideoFixture) defaults.logger.warn("No video fixture contributes a string model — serving the default video model set");
	const ids = modelIds.size > 0 ? [...modelIds] : DEFAULT_OPENROUTER_VIDEO_MODELS;
	if (res.destroyed || res.writableEnded) return;
	journal.add({
		method,
		path,
		headers: flattenHeaders(req.headers),
		body: null,
		response: {
			status: 200,
			fixture: null,
			...proxyAttemptFailed ? { source: "internal" } : {},
			...strictOverrideField(defaults.strict, req.headers)
		}
	});
	res.writeHead(200, { "Content-Type": "application/json" });
	res.end(JSON.stringify({ data: ids.map((id) => modelEntry(id)) }));
}
async function handleOpenRouterVideoCreate(req, res, raw, fixtures, journal, defaults, setCorsHeaders, jobs) {
	setCorsHeaders(res);
	const path = req.url ?? "/api/v1/videos";
	const method = req.method ?? "POST";
	let videoReq;
	try {
		videoReq = JSON.parse(raw);
	} catch (parseErr) {
		const detail = parseErr instanceof Error ? parseErr.message : "unknown";
		journal.add({
			method,
			path,
			headers: flattenHeaders(req.headers),
			body: null,
			response: {
				status: 400,
				fixture: null
			}
		});
		writeErrorResponse(res, 400, JSON.stringify({ error: {
			message: `Malformed JSON: ${detail}`,
			type: "invalid_request_error",
			code: "invalid_json"
		} }));
		return;
	}
	if (videoReq === null || typeof videoReq !== "object" || Array.isArray(videoReq)) {
		journal.add({
			method,
			path,
			headers: flattenHeaders(req.headers),
			body: null,
			response: {
				status: 400,
				fixture: null
			}
		});
		writeErrorResponse(res, 400, JSON.stringify({ error: {
			message: "Request body must be a JSON object",
			type: "invalid_request_error"
		} }));
		return;
	}
	const parsedBody = validationJournalBody(videoReq);
	if (typeof videoReq.prompt !== "string" || !videoReq.prompt) {
		journal.add({
			method,
			path,
			headers: flattenHeaders(req.headers),
			body: parsedBody,
			response: {
				status: 400,
				fixture: null
			}
		});
		const message = videoReq.prompt === void 0 ? "Missing required parameter: 'prompt'" : "Invalid type for parameter: 'prompt' must be a non-empty string";
		writeErrorResponse(res, 400, JSON.stringify({ error: {
			message,
			type: "invalid_request_error"
		} }));
		return;
	}
	if (videoReq.model !== void 0 && (typeof videoReq.model !== "string" || !videoReq.model)) {
		journal.add({
			method,
			path,
			headers: flattenHeaders(req.headers),
			body: parsedBody,
			response: {
				status: 400,
				fixture: null
			}
		});
		writeErrorResponse(res, 400, JSON.stringify({ error: {
			message: "Invalid type for parameter: 'model' must be a non-empty string",
			type: "invalid_request_error"
		} }));
		return;
	}
	const syntheticReq = {
		model: videoReq.model ?? DEFAULT_OPENROUTER_VIDEO_MODEL,
		messages: [{
			role: "user",
			content: videoReq.prompt
		}],
		_endpointType: "video",
		_context: getContext(req)
	};
	const testId = getTestId(req);
	const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(fixtures, syntheticReq, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform);
	if (fixture) {
		journal.incrementFixtureMatchCount(fixture, fixtures, testId);
		defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);
	} else {
		const snippet = videoReq.prompt.slice(0, 80);
		defaults.logger.debug(`No fixture matched for request (model=${syntheticReq.model}, msg="${snippet}")`);
	}
	if (applyChaos(res, fixture, defaults.chaos, req.headers, journal, {
		method,
		path,
		headers: flattenHeaders(req.headers),
		body: syntheticReq
	}, fixture ? "fixture" : resolveStrictMode(defaults.strict, req.headers) ? "internal" : defaults.record?.providers.openrouter ? "proxy" : "internal", defaults.registry, defaults.logger)) return;
	if (!fixture) {
		if (resolveStrictMode(defaults.strict, req.headers)) {
			const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);
			defaults.logger.error(strictNoMatchLogLine(method, path, skippedBySequenceOrTurn));
			journal.add({
				method,
				path,
				headers: flattenHeaders(req.headers),
				body: syntheticReq,
				response: {
					status: 503,
					fixture: null,
					...strictOverrideField(defaults.strict, req.headers)
				}
			});
			writeErrorResponse(res, 503, JSON.stringify({ error: {
				message: strictMessage,
				type: "invalid_request_error",
				code: "no_fixture_match"
			} }));
			return;
		}
		if (defaults.record) {
			if (await proxyOpenRouterVideoSubmit({
				req,
				res,
				raw,
				syntheticReq,
				record: defaults.record,
				journal,
				defaults,
				jobs,
				method,
				path
			}) === "handled") return;
		}
		journal.add({
			method,
			path,
			headers: flattenHeaders(req.headers),
			body: syntheticReq,
			response: {
				status: 404,
				fixture: null,
				...strictOverrideField(defaults.strict, req.headers)
			}
		});
		writeErrorResponse(res, 404, JSON.stringify({ error: {
			message: "No fixture matched",
			code: 404
		} }));
		return;
	}
	const worldGeneration = jobs.generation;
	const response = await resolveResponse(fixture, syntheticReq);
	if (isErrorResponse(response)) {
		if (res.destroyed || res.writableEnded) return;
		const status = response.status ?? 500;
		journal.add({
			method,
			path,
			headers: flattenHeaders(req.headers),
			body: syntheticReq,
			response: {
				status,
				fixture
			}
		});
		writeErrorResponse(res, status, serializeErrorResponse(response), { retryAfter: response.retryAfter });
		return;
	}
	if (!isVideoResponse(response)) {
		if (res.destroyed || res.writableEnded) return;
		journal.add({
			method,
			path,
			headers: flattenHeaders(req.headers),
			body: syntheticReq,
			response: {
				status: 500,
				fixture
			}
		});
		writeErrorResponse(res, 500, JSON.stringify({ error: {
			message: "Fixture response is not a video type",
			type: "server_error"
		} }));
		return;
	}
	if (res.destroyed || res.writableEnded) return;
	journal.add({
		method,
		path,
		headers: flattenHeaders(req.headers),
		body: syntheticReq,
		response: {
			status: 200,
			fixture
		}
	});
	const fixtureStatus = response.video.status;
	if (fixtureStatus === "processing") defaults.logger.warn(`Video fixture has status "processing" — treated as completed for /api/v1/videos jobs`);
	else if (fixtureStatus !== "completed" && fixtureStatus !== "failed") defaults.logger.warn(`Video fixture has unknown status "${fixtureStatus}" — treating as completed for /api/v1/videos jobs`);
	const jobId = crypto.randomUUID();
	const progression = resolveProgression(defaults.openRouterVideo);
	const job = {
		kind: "replay",
		jobId,
		status: "pending",
		pollCount: 0,
		pollsBeforeInProgress: progression.pollsBeforeInProgress,
		pollsBeforeCompleted: progression.pollsBeforeCompleted,
		video: { ...response.video }
	};
	if (progression.pollsBeforeCompleted === 0) job.status = terminalStatus(job);
	if (jobs.generation === worldGeneration) jobs.set(`${testId}:${jobId}`, job);
	else defaults.logger.warn(`OpenRouter video submit resolved after a fixtures reset — not inserting job ${jobId} into the new world (its polls will 404)`);
	res.writeHead(200, { "Content-Type": "application/json" });
	res.end(JSON.stringify({
		id: jobId,
		polling_url: `${requestBase(req, defaults.logger)}/api/v1/videos/${jobId}${testIdSuffix(testId, "?")}`,
		status: "pending"
	}));
}
const OPENROUTER_VIDEOS_PATH = "/api/v1/videos";
/**
* Proxy an unmatched video submit to the configured OpenRouter upstream and
* answer the client with a mock-rewritten envelope: a fresh aimock jobId and
* a polling_url pointing back at the mock (testId embedded). The upstream
* lifecycle is then driven interactively by the client's own polls — unlike
* fal's synchronous queue walk, nothing is polled server-side at submit.
*
* Returns "no_upstream" when record mode has no openrouter provider URL —
* the caller falls through to its 404 branch (fal convention).
*/
async function proxyOpenRouterVideoSubmit(args) {
	const { req, res, raw, syntheticReq, record, journal, defaults, jobs, method, path } = args;
	const upstreamBase = record.providers.openrouter;
	if (!upstreamBase) {
		defaults.logger.warn(`No upstream URL configured for provider "openrouter" — cannot proxy`);
		return "no_upstream";
	}
	const proxyError = (msg) => {
		defaults.logger.error(`OpenRouter video submit proxy failed: ${msg}`);
		if (res.destroyed || res.writableEnded) return "handled";
		journal.add({
			method,
			path,
			headers: flattenHeaders(req.headers),
			body: syntheticReq,
			response: {
				status: 502,
				fixture: null,
				source: "proxy",
				...strictOverrideField(defaults.strict, req.headers)
			}
		});
		writeErrorResponse(res, 502, JSON.stringify({ error: {
			message: `Proxy to upstream failed: ${msg}`,
			type: "proxy_error"
		} }));
		return "handled";
	};
	let submitUrl;
	let upstreamOrigin;
	try {
		submitUrl = resolveUpstreamUrl(upstreamBase, OPENROUTER_VIDEOS_PATH);
		upstreamOrigin = new URL(upstreamBase).origin;
	} catch (err) {
		return proxyError(`Invalid upstream URL: ${upstreamBase} (${err instanceof Error ? err.message : String(err)})`);
	}
	defaults.logger.warn(`NO FIXTURE MATCH — proxying video submit to ${upstreamBase}${OPENROUTER_VIDEOS_PATH}`);
	const worldGeneration = jobs.generation;
	let fetched;
	try {
		const upstreamRes = await fetch(submitUrl, {
			method: "POST",
			headers: buildForwardHeaders(req),
			body: raw,
			signal: upstreamTimeoutSignal(record)
		});
		fetched = {
			status: upstreamRes.status,
			contentType: upstreamRes.headers.get("content-type"),
			text: await readEnvelopeText(upstreamRes, record)
		};
	} catch (err) {
		return proxyError(err instanceof Error ? err.message : "Unknown proxy error");
	}
	if (fetched.status === 401 || fetched.status === 403) {
		defaults.logger.warn(`Upstream rejected the video submit (${fetched.status}) — relaying the upstream status`);
		if (res.destroyed || res.writableEnded) return "handled";
		journal.add({
			method,
			path,
			headers: flattenHeaders(req.headers),
			body: syntheticReq,
			response: {
				status: fetched.status,
				fixture: null,
				source: "proxy",
				...strictOverrideField(defaults.strict, req.headers)
			}
		});
		res.writeHead(fetched.status, { "Content-Type": fetched.contentType ?? "application/json" });
		res.end(fetched.text);
		return "handled";
	}
	let upstreamJobId;
	let envelope;
	{
		if (fetched.status < 200 || fetched.status >= 300) return proxyError(`Submit ${fetched.status}: ${fetched.text.slice(0, 200)}`);
		let parsed;
		try {
			parsed = JSON.parse(fetched.text);
		} catch {
			return proxyError(`Submit returned non-JSON: ${fetched.text.slice(0, 200)}`);
		}
		if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return proxyError("Submit response is not a JSON object");
		envelope = parsed;
		upstreamJobId = String(envelope.id ?? "").trim();
		if (!upstreamJobId) return proxyError("Submit response missing id");
	}
	let upstreamPollingUrl = resolveUpstreamUrl(upstreamBase, `${OPENROUTER_VIDEOS_PATH}/${encodeURIComponent(upstreamJobId)}`).toString();
	const envPolling = envelope.polling_url;
	if (typeof envPolling === "string" && envPolling) try {
		const parsedPolling = new URL(envPolling);
		if (parsedPolling.origin === upstreamOrigin) upstreamPollingUrl = parsedPolling.toString();
		else defaults.logger.warn(`Upstream polling_url origin ${parsedPolling.origin} differs from the configured provider origin ${upstreamOrigin} — using the constructed poll URL instead`);
	} catch {
		defaults.logger.warn(`Upstream polling_url is not a valid URL (${String(envPolling).slice(0, 100)}) — using the constructed poll URL instead`);
	}
	const testId = getTestId(req);
	const matchRequest = defaults.requestTransform ? defaults.requestTransform(syntheticReq) : syntheticReq;
	const jobId = crypto.randomUUID();
	const job = {
		kind: "record",
		jobId,
		status: "pending",
		upstreamJobId,
		upstreamPollingUrl,
		match: buildFixtureMatch(matchRequest, record)
	};
	if (jobs.generation === worldGeneration) jobs.set(`${testId}:${jobId}`, job);
	else defaults.logger.warn(`OpenRouter video submit for upstream job ${upstreamJobId} completed after a fixtures reset — not inserting the job into the new world (its polls will 404)`);
	if (res.destroyed || res.writableEnded) return "handled";
	journal.add({
		method,
		path,
		headers: flattenHeaders(req.headers),
		body: syntheticReq,
		response: {
			status: 200,
			fixture: null,
			source: "proxy",
			...strictOverrideField(defaults.strict, req.headers)
		}
	});
	res.writeHead(200, { "Content-Type": "application/json" });
	res.end(JSON.stringify({
		id: jobId,
		polling_url: `${requestBase(req, defaults.logger)}/api/v1/videos/${jobId}${testIdSuffix(testId, "?")}`,
		status: "pending"
	}));
	return "handled";
}
/**
* Default cap on the DECODED byte size embedded as `b64` in a recorded
* fixture (32 MB). Override per-server via
* `record.openRouterVideo.maxContentBytes` (0 = unlimited; negative or
* non-integer values are treated as the default, with a createServer warn).
* The cap protects disk AND memory: a capture whose upstream response
* DECLARES an over-cap Content-Length is skipped without downloading, while
* a response with no declared length is read as a stream with the byte count
* enforced during the read — on exceed the download aborts with nothing
* oversized retained in memory. In both cases the fixture is persisted
* without `b64` and the same-session job serves the placeholder MP4.
*/
const OPENROUTER_VIDEO_DEFAULT_MAX_CONTENT_BYTES = 32 * 1024 * 1024;
/**
* Proxy a status poll for a record-mode job 1:1 to the upstream and relay
* the result with mock-rewritten identifiers (rewriteRecordPollBody: id,
* polling_url, unsigned_urls; everything else verbatim). When the upstream
* reports `completed`, the rewritten body is relayed IMMEDIATELY and the
* eager capture (server-side fetch of unsigned_urls[0], forwarding the
* polling client's Bearer ONLY same-origin; persist; replay mutation) runs
* DETACHED in the background — see captureOpenRouterVideoRecordFixture — so
* an SDK poller is never blocked on a multi-minute download; the content
* handler (and every later poll) serves the job locally once the capture
* lands. `failed` persists a failed fixture synchronously on the relaying
* poll. Every post-await map mutation is identity-guarded
* (`jobs.get(key) === job`) so a stale response resolving after a concurrent
* poll's capture replaced the entry can never resurrect the detached record
* object over the terminal replay job.
* `cancelled`/`expired` (not representable in VideoResponse.status) pass
* through with a warn and are never recorded. Under `record.proxyOnly`
* nothing is captured, persisted, or mutated: terminal polls relay the
* rewritten upstream body, the upstream unsigned_urls are stored on the
* record job for the content endpoint to live-proxy, and every later poll
* keeps proxying upstream. Each successful proxied poll of a record job
* refreshes its TTL so a long generation cannot be evicted mid-recording.
*/
async function proxyOpenRouterVideoRecordPoll(args) {
	const { req, res, job, key, testId, fixtures, journal, defaults, jobs, method, path } = args;
	const logger = defaults.logger;
	const journalProxy = (status) => {
		journal.add({
			method,
			path,
			headers: flattenHeaders(req.headers),
			body: null,
			response: {
				status,
				fixture: null,
				source: "proxy",
				...strictOverrideField(defaults.strict, req.headers)
			}
		});
	};
	const proxyError = (msg) => {
		logger.error(`OpenRouter video poll proxy failed: ${msg}`);
		if (res.destroyed || res.writableEnded) return;
		journalProxy(502);
		writeErrorResponse(res, 502, JSON.stringify({ error: {
			message: `Proxy to upstream failed: ${msg}`,
			type: "proxy_error"
		} }));
	};
	const record = defaults.record;
	if (!record) {
		proxyError("record mode is no longer configured for an in-flight record job");
		return;
	}
	let fetched;
	try {
		const upstreamRes = await fetch(job.upstreamPollingUrl, {
			headers: buildForwardHeaders(req),
			signal: upstreamTimeoutSignal(record)
		});
		fetched = {
			status: upstreamRes.status,
			contentType: upstreamRes.headers.get("content-type"),
			text: await readEnvelopeText(upstreamRes, record)
		};
	} catch (err) {
		proxyError(err instanceof Error ? err.message : "Unknown proxy error");
		return;
	}
	if (fetched.status === 401 || fetched.status === 403) {
		logger.warn(`Upstream rejected the status poll for job ${job.upstreamJobId} (${fetched.status}) — relaying the upstream status`);
		if (res.destroyed || res.writableEnded) return;
		journalProxy(fetched.status);
		res.writeHead(fetched.status, { "Content-Type": fetched.contentType ?? "application/json" });
		res.end(fetched.text);
		return;
	}
	let upstreamBody;
	{
		if (fetched.status < 200 || fetched.status >= 300) {
			proxyError(`Status ${fetched.status}: ${fetched.text.slice(0, 200)}`);
			return;
		}
		let parsed;
		try {
			parsed = JSON.parse(fetched.text);
		} catch {
			proxyError(`Status returned non-JSON: ${fetched.text.slice(0, 200)}`);
			return;
		}
		if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
			proxyError("Status response is not a JSON object");
			return;
		}
		upstreamBody = parsed;
	}
	const relayJson = (body) => {
		if (res.destroyed || res.writableEnded) return;
		journalProxy(200);
		res.writeHead(200, { "Content-Type": "application/json" });
		res.end(JSON.stringify(body));
	};
	const relayBody = rewriteRecordPollBody({
		upstreamBody,
		job,
		req,
		testId,
		logger
	});
	const upstreamStatus = String(upstreamBody.status ?? "");
	if (upstreamStatus === "pending" || upstreamStatus === "in_progress") {
		if (jobs.get(key) === job) {
			if (!job.capturing && job.status !== "completed" && job.status !== "failed") job.status = upstreamStatus;
			jobs.set(key, job);
		}
		relayJson(relayBody);
		return;
	}
	if (upstreamStatus === "completed") {
		if (record.proxyOnly) {
			if (jobs.get(key) === job) {
				const urls = upstreamBody.unsigned_urls;
				job.status = "completed";
				if (Array.isArray(urls)) job.upstreamUnsignedUrls = [...urls];
				jobs.set(key, job);
			}
			relayJson(relayBody);
			return;
		}
		if (job.capturing || jobs.get(key) !== job) {
			if (jobs.get(key) === job) {
				const freshUrls = upstreamBody.unsigned_urls;
				if (Array.isArray(freshUrls)) job.upstreamUnsignedUrls = [...freshUrls];
				jobs.set(key, job);
			}
			relayJson(relayBody);
			return;
		}
		const urls = upstreamBody.unsigned_urls;
		job.capturing = true;
		job.status = "completed";
		if (Array.isArray(urls)) job.upstreamUnsignedUrls = [...urls];
		jobs.set(key, job);
		relayJson(relayBody);
		captureOpenRouterVideoRecordFixture({
			req,
			job,
			key,
			testId,
			fixtures,
			defaults,
			jobs,
			record,
			upstreamBody
		});
		return;
	}
	if (upstreamStatus === "failed") {
		if (record.proxyOnly) {
			if (jobs.get(key) === job) {
				job.status = "failed";
				jobs.set(key, job);
			}
			relayJson(relayBody);
			return;
		}
		if (jobs.get(key) !== job) {
			relayJson(relayBody);
			return;
		}
		const rawError = upstreamBody.error;
		let error;
		if (typeof rawError === "string" && rawError) error = rawError;
		else if (rawError !== null && typeof rawError === "object" && !Array.isArray(rawError)) {
			const message = rawError.message;
			if (typeof message === "string" && message) error = message;
		}
		if (error === void 0 && rawError !== void 0 && rawError !== null) logger.warn(`Upstream video job ${job.upstreamJobId} failed with an unusable error value (${JSON.stringify(rawError).slice(0, 100)}) — recording the fixture without an error message (replay serves the default)`);
		const video = {
			id: job.upstreamJobId,
			status: "failed",
			...error !== void 0 ? { error } : {}
		};
		const persistResult = persistFixture({
			record,
			providerKey: "openrouter",
			testId,
			fixture: {
				match: job.match,
				response: { video }
			},
			fixtures,
			logger
		});
		if (persistResult.kind === "failed" && !res.headersSent) res.setHeader("X-AIMock-Record-Error", sanitizeHeaderValue(persistResult.error));
		jobs.set(key, {
			kind: "replay",
			jobId: job.jobId,
			status: "failed",
			pollCount: 0,
			pollsBeforeInProgress: 0,
			pollsBeforeCompleted: 0,
			video: { ...video }
		});
		relayJson(relayBody);
		return;
	}
	logger.warn(`Upstream video job ${job.upstreamJobId} reported status "${upstreamStatus}" — not representable in fixture video.status; passing through without recording`);
	if (jobs.get(key) === job) jobs.set(key, job);
	relayJson(relayBody);
}
/**
* Detached eager-capture sequence for a completed record job. Runs AFTER the
* triggering completed poll has been relayed (the client is never blocked on
* a potentially multi-minute video download): fetches `unsigned_urls[0]`
* server-side (the polling client's Bearer is forwarded ONLY same-origin),
* persists the fixture, and mutates the map entry into a terminal replay job.
* Every failure is handled internally and the returned promise NEVER
* rejects. Failure semantics: a capture FAILURE — no usable
* unsigned_urls, an invalid content URL, or a content fetch that errors on
* headers, status, or body — persists NOTHING and leaves the job a live
* proxy, so the next completed poll retries the capture (each failure
* warns). The ONLY degraded persist is the over-cap path (declared or
* streamed): retrying would always re-exceed the cap, so the b64-less
* fixture + placeholder is the deliberate, permanent outcome. The capturing
* window opened by the caller is closed in the finally when the map still
* holds this record job; on the success path the entry was just replaced and
* the detached record object deliberately KEEPS capturing=true so a
* concurrent poll that read it before the replacement still early-relays
* instead of starting a second capture.
*/
async function captureOpenRouterVideoRecordFixture(args) {
	const { req, job, key, testId, fixtures, defaults, jobs, record, upstreamBody } = args;
	const logger = defaults.logger;
	const urls = upstreamBody.unsigned_urls;
	const warnings = [];
	let capturedB64;
	try {
		const usage = upstreamBody.usage;
		const rawCost = usage !== null && typeof usage === "object" && !Array.isArray(usage) ? usage.cost : void 0;
		const cost = typeof rawCost === "number" ? rawCost : void 0;
		const rawCap = record.openRouterVideo?.maxContentBytes;
		const cap = rawCap !== void 0 && Number.isInteger(rawCap) && rawCap >= 0 ? rawCap : OPENROUTER_VIDEO_DEFAULT_MAX_CONTENT_BYTES;
		const urlArray = Array.isArray(urls) ? urls : void 0;
		if (urlArray && urlArray.length > 1) logger.warn(`Upstream video job ${job.upstreamJobId} reported ${urlArray.length} unsigned_urls — only index 0 is captured; post-capture replays serve the index-0 bytes for every index`);
		const firstRaw = urlArray?.[0];
		const firstUrl = typeof firstRaw === "string" && firstRaw ? firstRaw : void 0;
		if (!firstUrl) {
			if (urlArray && urlArray.length > 0) logger.warn(`Upstream video job ${job.upstreamJobId} completed with an unusable unsigned_urls[0] (${JSON.stringify(firstRaw).slice(0, 100)}) — capture skipped, nothing persisted; the next completed poll retries`);
			else logger.warn(`Upstream video job ${job.upstreamJobId} completed without unsigned_urls — capture skipped, nothing persisted; the next completed poll retries`);
			return;
		}
		let contentUrl;
		try {
			contentUrl = new URL(firstUrl);
		} catch {
			logger.warn(`Upstream unsigned_urls[0] is not a valid URL (${firstUrl.slice(0, 100)}) — capture skipped, nothing persisted; the next completed poll retries`);
			return;
		}
		{
			const providerBase = record.providers.openrouter;
			let providerOrigin;
			try {
				providerOrigin = new URL(providerBase ?? job.upstreamPollingUrl).origin;
			} catch {
				try {
					providerOrigin = new URL(job.upstreamPollingUrl).origin;
				} catch {
					logger.error(`OpenRouter video capture for job ${job.upstreamJobId} aborted: cannot determine the provider origin (invalid provider URL and polling URL) — refusing to forward credentials`);
					return;
				}
			}
			const headers = buildForwardHeaders(req);
			if (contentUrl.origin !== providerOrigin) {
				delete headers.authorization;
				logger.warn(`Upstream unsigned_urls[0] origin ${contentUrl.origin} differs from the provider origin ${providerOrigin} — fetching content WITHOUT the client's Authorization header`);
			}
			try {
				const contentRes = await fetchHeadersWithTimeout(contentUrl, headers, record);
				if (!contentRes.ok) {
					let sample = "";
					try {
						const read = await readBodyIdle(contentRes, record, OPENROUTER_VIDEO_ERROR_BODY_CAP);
						if (!read.overCap) sample = read.buf.toString("utf8").slice(0, 200);
					} catch {}
					throw new Error(`Content ${contentRes.status}${sample ? `: ${sample}` : ""}`);
				}
				const declaredLength = Number(contentRes.headers.get("content-length") ?? NaN);
				if (cap > 0 && Number.isFinite(declaredLength) && declaredLength > cap) {
					contentRes.body?.cancel().catch(() => {});
					logger.warn(`Captured video for job ${job.upstreamJobId} declares Content-Length ${declaredLength} — over maxContentBytes (${cap}); skipping the download; fixture persisted WITHOUT b64 (content serves the placeholder MP4)`);
					warnings.push(`Declared content length (${declaredLength} bytes) exceeded maxContentBytes (${cap}) — download skipped, b64 omitted`);
				} else {
					const read = await readBodyIdle(contentRes, record, cap);
					if (read.overCap) {
						logger.warn(`Captured video for job ${job.upstreamJobId} streamed past maxContentBytes (${cap}) — download aborted at ${read.bytesRead} bytes; fixture persisted WITHOUT b64 (content serves the placeholder MP4)`);
						warnings.push(`Captured video exceeded maxContentBytes (${cap}) — download aborted, b64 omitted`);
					} else capturedB64 = read.buf.toString("base64");
				}
			} catch (err) {
				const msg = err instanceof Error ? err.message : "Unknown capture error";
				logger.warn(`OpenRouter video content capture failed (${msg}) — nothing persisted; the job keeps proxying live and the next completed poll retries`);
				return;
			}
		}
		const video = {
			id: job.upstreamJobId,
			status: "completed",
			...capturedB64 !== void 0 ? { b64: capturedB64 } : {},
			...cost !== void 0 ? { cost } : {}
		};
		if (jobs.get(key) !== job) {
			logger.warn(`OpenRouter video capture for job ${job.upstreamJobId} discarded: the job map no longer holds this job (fixtures reset or TTL eviction mid-capture) — nothing persisted`);
			return;
		}
		persistFixture({
			record,
			providerKey: "openrouter",
			testId,
			fixture: {
				match: job.match,
				response: { video }
			},
			fixtures,
			warnings,
			logger
		});
		if (jobs.get(key) === job) jobs.set(key, {
			kind: "replay",
			jobId: job.jobId,
			status: "completed",
			pollCount: 0,
			pollsBeforeInProgress: 0,
			pollsBeforeCompleted: 0,
			video: { ...video }
		});
	} catch (err) {
		const msg = err instanceof Error ? err.message : String(err);
		logger.error(`OpenRouter video capture for job ${job.upstreamJobId} failed unexpectedly (${msg}) — fixture not persisted; the job keeps proxying live`);
	} finally {
		if (jobs.get(key) === job) job.capturing = false;
	}
}

//#endregion
export { OPENROUTER_VIDEO_DEFAULT_MAX_CONTENT_BYTES, OPENROUTER_VIDEO_MAX_ENTRIES, OpenRouterVideoJobMap, handleOpenRouterVideoContent, handleOpenRouterVideoCreate, handleOpenRouterVideoModels, handleOpenRouterVideoStatus };
//# sourceMappingURL=openrouter-video.js.map