const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
const require_constants = require('./constants.cjs');
const require_model_utils = require('./model-utils.cjs');
const require_helpers = require('./helpers.cjs');
const require_router = require('./router.cjs');
const require_sse_writer = require('./sse-writer.cjs');
const require_stream_collapse = require('./stream-collapse.cjs');
const require_url = require('./url.cjs');
const require_provider_auth = require('./provider-auth.cjs');
let node_http = require("node:http");
node_http = require_runtime.__toESM(node_http);
let node_crypto = require("node:crypto");
node_crypto = require_runtime.__toESM(node_crypto);
let node_https = require("node:https");
node_https = require_runtime.__toESM(node_https);
let node_fs = require("node:fs");
node_fs = require_runtime.__toESM(node_fs);
let node_path = require("node:path");
node_path = require_runtime.__toESM(node_path);
let node_string_decoder = require("node:string_decoder");

//#region src/recorder.ts
/** Headers to strip when proxying — hop-by-hop (RFC 2616 §13.5.1) + client-set. */
/**
* Default ceiling (bytes) for the in-memory proxy-path buffer. Chosen well
* under V8's ~512 MiB max string length so `rawBuffer.toString()` /
* stream-collapse never throws `RangeError: Invalid string length`, and so a
* single huge proxied response cannot spike the heap unbounded. Overridable
* via `RecordConfig.maxProxyBufferBytes` / `--max-proxy-buffer-bytes`.
*/
const DEFAULT_MAX_PROXY_BUFFER_BYTES = 64 * 1024 * 1024;
/**
* Default ceiling for the number of SSE/NDJSON/EventStream frames whose
* per-frame state (`frameTimestamps`, parse buffers) aimock retains for a
* single proxied response. Frame state is count-indexed, not byte-sized, so a
* long-lived / never-ending stream accumulates `frameTimestamps` entries (and,
* if a frame never completes, parse-buffer bytes) UNBOUNDED even when the byte
* cap is generous — observed as multi-GB heap growth over many hours from a few
* long nested-sub-agent streams. Tripping truncation on EITHER bytes OR frame
* count bounds both. 5M frames is generous for any real response (a normal
* completion is hundreds-to-thousands of frames) while still bounding a runaway
* stream to ~tens of MB of frame state. Overridable via
* `RecordConfig.maxProxyBufferFrames` / `--max-proxy-buffer-frames`.
*/
const DEFAULT_MAX_PROXY_BUFFER_FRAMES = 5e6;
/**
* Absolute hard ceiling (bytes) for any in-memory proxy buffer, independent of
* the configurable `maxProxyBufferBytes`. V8's maximum STRING length on 64-bit
* is 2^29 - 1 (~512 MiB of UTF-16 code units), and the proxy buffer is
* eventually stringified via `rawBuffer.toString()` for collapse/relay — so the
* BYTE buffer must stay safely under that string limit or the toString throws
* `RangeError: Invalid string length`. 256 MiB of bytes is well under the
* ~512 MiB string-length boundary (these are different units — bytes vs UTF-16
* code units — but 256 MiB of bytes can never decode to more than 256 Mi code
* units, comfortably below 2^29 - 1). Used to clamp the configurable cap AND to
* bound the non-progressive relay buffer that must be retained past a cap trip.
*/
const PROXY_BUFFER_HARD_CEILING = 256 * 1024 * 1024;
/**
* Test-only override of the effective hard ceiling. Lets the proxy-buffer
* enforcement suite exercise the >hard-ceiling fail-loud path with a small
* body instead of streaming 256 MiB. `undefined` (the default) uses the real
* `PROXY_BUFFER_HARD_CEILING`. NEVER set from production code.
*/
let proxyBufferHardCeilingOverride;
/** Effective hard ceiling, honoring any active test-only override. */
function effectiveHardCeiling() {
	return proxyBufferHardCeilingOverride ?? PROXY_BUFFER_HARD_CEILING;
}
const STRIP_HEADERS = new Set([
	"connection",
	"keep-alive",
	"transfer-encoding",
	"te",
	"trailer",
	"upgrade",
	"proxy-authorization",
	"proxy-authenticate",
	"host",
	"content-length",
	"cookie",
	"accept-encoding",
	"x-test-id",
	"x-aimock-strict",
	"x-aimock-context"
]);
/**
* Build the header set forwarded to an upstream provider from an incoming
* request: everything except hop-by-hop, client-set, and mock-internal
* headers (STRIP_HEADERS, plus the x-aimock-chaos-* prefix family). Shared
* by the generic recorder proxy and the OpenRouter-video live lifecycle
* proxy.
*/
function buildForwardHeaders(req) {
	const out = {};
	for (const [name, val] of Object.entries(req.headers)) {
		const lower = name.toLowerCase();
		if (val === void 0 || STRIP_HEADERS.has(lower) || lower.startsWith("x-aimock-chaos-")) continue;
		out[name] = Array.isArray(val) ? val.join(", ") : val;
	}
	return out;
}
/**
* Make an arbitrary string safe to set as an HTTP header value. Node's
* `res.setHeader` throws ERR_INVALID_CHAR on anything outside Latin-1 (plus
* control characters) — and persist errors embed filesystem paths verbatim,
* so a Unicode fixture path would turn a recoverable record failure into a
* 500. Out-of-range characters are replaced with `?` rather than stripped so
* the value's shape stays legible.
*/
function sanitizeHeaderValue(value) {
	return value.replace(/[^\t\x20-\x7e\x80-\xff]/g, "?");
}
/**
* Resolve a `content-type` header value to a single string. Node's
* `http.IncomingHttpHeaders` types `content-type` as `string | string[]`, and
* a constructed/proxied header object CAN carry an array (e.g. duplicated
* header lines surfaced by some HTTP stacks). `join(", ")`-ing such an array
* produces a malformed `Content-Type: application/json, text/html` value — pick
* the FIRST element instead, which is the value the client should act on.
* Returns `""` when the header is absent or an empty array.
*/
function pickContentType(contentType) {
	if (Array.isArray(contentType)) return contentType.length > 0 ? contentType[0] : "";
	return contentType ?? "";
}
/**
* Write a built fixture to disk (snapshot vs. timestamp file layout) and, when
* the match is non-empty, register it in the in-memory cache so subsequent
* identical requests match. Extracted from `proxyAndRecord` so the fal
* queue-walk recorder (which makes multiple upstream calls before knowing the
* final body) can share the same persistence behavior without re-implementing
* snapshot-mode merging and warnings.
*/
function persistFixture(opts) {
	const { record, providerKey, testId, fixture, fixtures, warnings = [], logger } = opts;
	const m = fixture.match;
	const isEmptyMatch = m.userMessage === void 0 && m.inputText === void 0 && m.endpoint === void 0;
	if (record.proxyOnly) {
		logger.info(`Proxied ${providerKey} request (proxy-only mode)`);
		return { kind: "skipped" };
	}
	if (isEmptyMatch) logger.warn("Recorded fixture has empty match criteria — skipping in-memory registration");
	const fixturePath = record.fixturePath ?? "./fixtures/recorded";
	let isSnapshotMode = testId !== require_constants.DEFAULT_TEST_ID;
	let filepath;
	let mergeExisting = false;
	if (isSnapshotMode) {
		const slug = require_helpers.slugifyTestId(testId);
		if (!slug) {
			isSnapshotMode = false;
			const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
			filepath = node_path.join(fixturePath, `${providerKey}-${timestamp}-${node_crypto.randomUUID().slice(0, 8)}.json`);
		} else {
			filepath = node_path.join(fixturePath, slug, `${providerKey}.json`);
			mergeExisting = true;
		}
	} else {
		const timestampFile = `${providerKey}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${node_crypto.randomUUID().slice(0, 8)}.json`;
		const contextSegment = fixture.match.context ? require_helpers.slugifyContext(fixture.match.context) : void 0;
		filepath = contextSegment ? node_path.join(fixturePath, contextSegment, timestampFile) : node_path.join(fixturePath, timestampFile);
	}
	const fileWarnings = [...isEmptyMatch ? ["Empty match criteria — this fixture will not match any request"] : [], ...warnings];
	try {
		node_fs.mkdirSync(node_path.dirname(filepath), { recursive: true });
		let fileContent;
		if (mergeExisting && node_fs.existsSync(filepath)) try {
			const existing = JSON.parse(node_fs.readFileSync(filepath, "utf-8"));
			const existingFixtures = Array.isArray(existing.fixtures) ? existing.fixtures : void 0;
			if (existingFixtures === void 0 && existing.fixtures !== void 0) logger.warn(`Existing fixture file ${filepath} has a non-array "fixtures" — discarding it and starting fresh`);
			fileContent = { fixtures: [...existingFixtures ?? [], fixture] };
			if (Array.isArray(existing._warnings)) fileWarnings.unshift(...existing._warnings.filter((w) => typeof w === "string"));
			else if (typeof existing._warning === "string" && existing._warning) fileWarnings.unshift(...existing._warning.split("; "));
		} catch (mergeErr) {
			const msg = mergeErr instanceof Error ? mergeErr.message : "unknown";
			logger.warn(`Could not read existing fixture file ${filepath} (${msg}) — overwriting`);
			fileContent = { fixtures: [fixture] };
		}
		else fileContent = { fixtures: [fixture] };
		if (fileWarnings.length > 0) {
			const dedupedWarnings = [...new Set(fileWarnings)];
			fileContent._warnings = dedupedWarnings;
			fileContent._warning = dedupedWarnings.join("; ");
		}
		const tmpPath = filepath + ".tmp." + process.pid;
		node_fs.writeFileSync(tmpPath, JSON.stringify(fileContent, null, 2), "utf-8");
		node_fs.renameSync(tmpPath, filepath);
		if (!isEmptyMatch) fixtures.push(fixture);
		logger.warn(`Response recorded → ${filepath}`);
		return {
			kind: "written",
			filepath
		};
	} catch (err) {
		const msg = err instanceof Error ? err.message : "Unknown filesystem error";
		logger.error(`Failed to save fixture to disk: ${msg}`);
		return {
			kind: "failed",
			error: msg
		};
	}
}
/**
* Proxy an unmatched request to the real upstream provider, record the
* response as a fixture on disk and in memory, then relay the response
* back to the original client.
*/
async function proxyAndRecord(req, res, request, providerKey, pathname, fixtures, defaults, rawBody, options) {
	const record = defaults.record;
	if (!record) return "not_configured";
	const providers = record.providers;
	const lookupKey = providerKey === "gemini-interactions" ? "gemini" : providerKey;
	const upstreamUrl = providers[lookupKey];
	if (!upstreamUrl) {
		defaults.logger.warn(`No upstream URL configured for provider "${providerKey}" — cannot proxy`);
		return "not_configured";
	}
	let target;
	try {
		target = require_url.resolveUpstreamUrl(upstreamUrl, pathname);
	} catch (err) {
		const msg = err instanceof Error ? err.message : String(err);
		defaults.logger.error(`Invalid upstream URL for provider "${providerKey}": ${upstreamUrl} (${msg})`);
		require_sse_writer.writeErrorResponse(res, 502, JSON.stringify({ error: {
			message: `Invalid upstream URL: ${upstreamUrl}`,
			type: "proxy_error"
		} }));
		return "relayed";
	}
	defaults.logger.warn(`NO FIXTURE MATCH — proxying to ${upstreamUrl}${pathname}`);
	const forwardHeaders = buildForwardHeaders(req);
	require_provider_auth.applyProviderAuth(forwardHeaders, target, providerKey, record.providerKeys?.[lookupKey]);
	const requestBody = rawBody ?? JSON.stringify(request);
	let upstreamStatus;
	let upstreamHeaders;
	let upstreamBody;
	let rawBuffer;
	let bufferTruncated = false;
	let truncationCap;
	let totalBytes = 0;
	let hardCeilingExceeded = false;
	let streamedToClient = false;
	let clientDisconnected = false;
	let sawDone = false;
	let frameTimestamps = [];
	let streamStartTime = 0;
	const maxProxyBufferBytes = clampMaxBufferBytes(record.maxProxyBufferBytes);
	const maxProxyBufferFrames = clampMaxBufferFrames(record.maxProxyBufferFrames);
	try {
		const result = await makeUpstreamRequest(target, forwardHeaders, requestBody, res, req.method, defaults.logger, {
			upstreamTimeoutMs: record.upstreamTimeoutMs,
			bodyTimeoutMs: record.bodyTimeoutMs
		}, maxProxyBufferBytes, maxProxyBufferFrames);
		upstreamStatus = result.status;
		upstreamHeaders = result.headers;
		upstreamBody = result.body;
		rawBuffer = result.rawBuffer;
		streamedToClient = result.streamedToClient;
		clientDisconnected = result.clientDisconnected;
		sawDone = result.sawDone;
		frameTimestamps = result.frameTimestamps;
		streamStartTime = result.streamStartTime;
		bufferTruncated = result.bufferTruncated;
		truncationCap = result.truncationCap;
		totalBytes = result.totalBytes;
		hardCeilingExceeded = result.hardCeilingExceeded;
	} catch (err) {
		const msg = err instanceof Error ? err.message : "Unknown proxy error";
		defaults.logger.error(`Proxy request failed: ${msg}`);
		if (!res.headersSent) {
			res.writeHead(502, { "Content-Type": "application/json" });
			res.end(JSON.stringify({ error: {
				message: `Proxy to upstream failed: ${msg}`,
				type: "proxy_error"
			} }));
		} else res.destroy();
		return "relayed";
	}
	if (bufferTruncated) {
		const capDetail = truncationCap === "frame" ? `the ${maxProxyBufferFrames}-frame cap` : `the ${maxProxyBufferBytes}-byte cap`;
		if (!streamedToClient && hardCeilingExceeded && !res.headersSent) {
			const ceilingMiB = Math.round(PROXY_BUFFER_HARD_CEILING / (1024 * 1024));
			defaults.logger.error(`Upstream response exceeded the ${ceilingMiB} MiB proxy hard ceiling (saw ${totalBytes} bytes) on a non-progressive body — cannot relay the full body and refusing to relay a truncated one; returning 502`);
			res.writeHead(502, { "Content-Type": "application/json" });
			res.end(JSON.stringify({ error: {
				message: `Upstream response exceeds ${ceilingMiB}MiB proxy ceiling`,
				type: "proxy_error"
			} }));
			return "relayed";
		}
		defaults.logger.warn(`Upstream response exceeded ${capDetail} (saw ${totalBytes} bytes) — relayed to client, recording skipped`);
		if (!streamedToClient && !res.headersSent) {
			const clientStatus = upstreamStatus >= 200 && upstreamStatus < 300 ? 200 : 502;
			const relayHeaders = {};
			const ct = upstreamHeaders["content-type"];
			relayHeaders["Content-Type"] = (Array.isArray(ct) ? ct.join(", ") : ct ?? "") || "application/json";
			res.writeHead(clientStatus, relayHeaders);
			res.end(rawBuffer);
		} else if (!res.writableEnded) res.end();
		return "relayed";
	}
	const contentType = upstreamHeaders["content-type"];
	const ctString = pickContentType(contentType);
	const isBinaryStream = ctString.toLowerCase().includes("application/vnd.amazon.eventstream");
	const collapsed = require_stream_collapse.collapseStreamingResponse(ctString, providerKey, isBinaryStream ? rawBuffer : upstreamBody, defaults.logger);
	let fixtureResponse;
	if (ctString.toLowerCase().startsWith("audio/") && upstreamStatus >= 200 && upstreamStatus < 300) {
		const audioFormat = ctString.toLowerCase().replace("audio/", "").replace("mpeg", "mp3").split(";")[0].trim();
		if (rawBuffer.length === 0) defaults.logger.warn("Audio response had a zero-length body — recording an empty audio fixture");
		fixtureResponse = {
			audio: rawBuffer.toString("base64"),
			...audioFormat && audioFormat !== "mp3" ? { format: audioFormat } : {}
		};
	} else if (collapsed) {
		defaults.logger.warn(`Streaming response detected (${ctString}) — collapsing to fixture`);
		if (collapsed.truncated) defaults.logger.warn("Bedrock EventStream: CRC mismatch — response may be truncated");
		if (collapsed.droppedChunks && collapsed.droppedChunks > 0) defaults.logger.warn(`${collapsed.droppedChunks} chunk(s) dropped during stream collapse${collapsed.firstDroppedSample ? ` — first: ${collapsed.firstDroppedSample}` : ""}`);
		if (collapsed.harmonyUnparsed) defaults.logger.warn(`Harmony tokens present but unparseable — content preserved verbatim${collapsed.harmonyNote ? ` (${collapsed.harmonyNote})` : ""}`);
		if (collapsed.audioB64) {
			const audioToolCallsSpread = collapsed.toolCalls && collapsed.toolCalls.length > 0 ? { toolCalls: collapsed.toolCalls.map((tc) => ({
				...tc,
				name: tc.name ?? "",
				arguments: tc.arguments ?? "{}"
			})) } : {};
			const audioContentSpread = collapsed.content ? { content: collapsed.content } : {};
			const audioReasoningSpread = collapsed.reasoning ? { reasoning: collapsed.reasoning } : {};
			fixtureResponse = {
				audio: {
					b64Json: collapsed.audioB64,
					contentType: collapsed.audioMimeType ?? "audio/mpeg"
				},
				...audioToolCallsSpread,
				...audioContentSpread,
				...audioReasoningSpread
			};
		} else if (collapsed.content === "" && (!collapsed.toolCalls || collapsed.toolCalls.length === 0)) {
			defaults.logger.warn("Stream collapse produced empty content — fixture may be incomplete");
			const reasoningSpread = collapsed.reasoning ? { reasoning: collapsed.reasoning } : {};
			const reasoningSignatureSpread = collapsed.reasoning && collapsed.reasoningSignature ? { reasoningSignature: collapsed.reasoningSignature } : {};
			logDroppedReasoningSignature(defaults.logger, collapsed.reasoning, collapsed.reasoningSignature);
			const redactedThinkingSpread = collapsed.redactedThinking?.length ? { redactedThinking: collapsed.redactedThinking } : {};
			const webSearchesSpread = collapsed.webSearches?.length ? { webSearches: collapsed.webSearches } : {};
			fixtureResponse = {
				content: collapsed.content ?? "",
				...reasoningSpread,
				...reasoningSignatureSpread,
				...redactedThinkingSpread,
				...webSearchesSpread
			};
		} else {
			const reasoningSpread = collapsed.reasoning ? { reasoning: collapsed.reasoning } : {};
			const reasoningSignatureSpread = collapsed.reasoning && collapsed.reasoningSignature ? { reasoningSignature: collapsed.reasoningSignature } : {};
			logDroppedReasoningSignature(defaults.logger, collapsed.reasoning, collapsed.reasoningSignature);
			const redactedThinkingSpread = collapsed.redactedThinking?.length ? { redactedThinking: collapsed.redactedThinking } : {};
			const webSearchesSpread = collapsed.webSearches?.length ? { webSearches: collapsed.webSearches } : {};
			if (collapsed.toolCalls && collapsed.toolCalls.length > 0) {
				const sanitizedToolCalls = collapsed.toolCalls.map((tc) => ({
					...tc,
					name: tc.name ?? "",
					arguments: tc.arguments ?? "{}"
				}));
				if (collapsed.content) {
					const blocksSpread = collapsed.blocks?.length ? { blocks: collapsed.blocks } : {};
					fixtureResponse = {
						content: collapsed.content,
						toolCalls: sanitizedToolCalls,
						...blocksSpread,
						...reasoningSpread,
						...reasoningSignatureSpread,
						...redactedThinkingSpread,
						...webSearchesSpread
					};
				} else fixtureResponse = {
					toolCalls: sanitizedToolCalls,
					...reasoningSpread,
					...reasoningSignatureSpread,
					...redactedThinkingSpread,
					...webSearchesSpread
				};
			} else fixtureResponse = {
				content: collapsed.content ?? "",
				...reasoningSpread,
				...reasoningSignatureSpread,
				...redactedThinkingSpread,
				...webSearchesSpread
			};
		}
	} else {
		let parsedResponse = null;
		try {
			parsedResponse = JSON.parse(upstreamBody);
		} catch (parseErr) {
			const msg = parseErr instanceof Error ? parseErr.message : "unknown";
			defaults.logger.warn(`Upstream response is not valid JSON (${msg}) — saving as error fixture`);
		}
		if (request._endpointType === "fal" && parsedResponse !== null) {
			const obj = parsedResponse;
			if (typeof obj.error === "object" && obj.error !== null && typeof obj.error.message === "string") {
				const err = obj.error;
				fixtureResponse = {
					error: {
						message: String(err.message ?? "Unknown error"),
						type: String(err.type ?? "api_error"),
						code: err.code ? String(err.code) : void 0
					},
					status: upstreamStatus
				};
			} else fixtureResponse = {
				json: parsedResponse,
				status: upstreamStatus
			};
		} else fixtureResponse = buildFixtureResponse(parsedResponse, upstreamStatus, defaults.logger);
	}
	if (clientDisconnected) {
		if (!sawDone) {
			defaults.logger.warn("Client disconnected mid-stream — skipping fixture save to avoid truncated data");
			return "relayed";
		}
		defaults.logger.warn("Client closed connection before upstream end — upstream response completed, recording full fixture");
	}
	let recordedTimings;
	if (frameTimestamps.length > 1) {
		const ts = frameTimestamps;
		recordedTimings = {
			ttftMs: ts[0] - streamStartTime,
			interChunkDelaysMs: ts.slice(1).map((t, i) => t - ts[i]),
			totalDurationMs: ts[ts.length - 1] - streamStartTime
		};
	}
	const matchRequest = defaults.requestTransform ? defaults.requestTransform(request) : request;
	const metadata = buildFixtureMetadata(request);
	const fixture = {
		match: buildFixtureMatch(matchRequest, defaults.record),
		response: fixtureResponse,
		...recordedTimings && { recordedTimings },
		...metadata && { metadata }
	};
	const persistWarnings = [];
	if (collapsed?.truncated) persistWarnings.push("Stream response was truncated — fixture may be incomplete");
	const persistResult = persistFixture({
		record,
		providerKey,
		testId: require_helpers.getTestId(req),
		fixture,
		fixtures,
		warnings: persistWarnings,
		logger: defaults.logger
	});
	if (persistResult.kind === "failed") {
		if (!res.headersSent) res.setHeader("X-AIMock-Record-Error", sanitizeHeaderValue(persistResult.error));
		else defaults.logger.warn(`Cannot set X-AIMock-Record-Error header — headers already sent`);
		defaults.logger.warn(`Response relayed but NOT saved to disk — see error above`);
	}
	if (streamedToClient) {
		if (options?.beforeWriteResponse && options.onHookBypassed) {
			const bypassReason = isBinaryStream ? "binary_streamed" : ctString.toLowerCase().includes("application/x-ndjson") ? "ndjson_streamed" : "sse_streamed";
			try {
				options.onHookBypassed(bypassReason);
			} catch (err) {
				defaults.logger.warn(`onHookBypassed callback threw: ${err instanceof Error ? err.message : String(err)}`);
			}
		}
	} else {
		if (options?.beforeWriteResponse) {
			let handled;
			try {
				handled = await options.beforeWriteResponse({
					status: upstreamStatus,
					contentType: ctString,
					body: rawBuffer
				});
			} catch (err) {
				const msg = err instanceof Error ? err.message : String(err);
				throw new Error(`beforeWriteResponse hook failed for ${providerKey}: ${msg}`);
			}
			if (handled) return "handled_by_hook";
		}
		const clientStatus = upstreamStatus >= 200 && upstreamStatus < 300 ? 200 : 502;
		const isAudioRelay = ctString.toLowerCase().startsWith("audio/");
		const relayHeaders = {};
		const clientCt = clientStatus >= 200 && clientStatus < 300 || !isAudioRelay ? ctString || "application/json" : "application/json";
		if (clientCt) relayHeaders["Content-Type"] = clientCt;
		res.writeHead(clientStatus, relayHeaders);
		res.end(isBinaryStream || isAudioRelay ? rawBuffer : upstreamBody);
	}
	return "relayed";
}
/**
* Decodes a sequence of byte chunks to UTF-8 text for SSE/NDJSON frame
* splitting on the streamed-capture path. Wraps Node's StringDecoder so a
* multibyte UTF-8 character (CJK, emoji, ...) whose bytes are split across a
* TCP chunk boundary buffers across chunks instead of decoding to U+FFFD
* replacement characters — decoding each chunk independently with
* Buffer#toString() would corrupt the recorded frame text.
*/
var StreamingFrameDecoder = class {
	decoder = new node_string_decoder.StringDecoder("utf8");
	/** Decode a chunk, holding back any trailing partial multibyte sequence. */
	write(chunk) {
		return this.decoder.write(chunk);
	}
	/** Flush any buffered bytes once the stream has ended. */
	end() {
		return this.decoder.end();
	}
};
/**
* Sanitize a configured timeout: non-finite or non-positive values fall back
* to the default. Shared with the OpenRouter-video live lifecycle proxy so
* `record.upstreamTimeoutMs` is clamped identically on every proxy path.
*/
function clampTimeout(value, fallback) {
	if (value == null || !Number.isFinite(value) || value <= 0) return fallback;
	return value;
}
/**
* Sanitize the configured proxy-buffer cap: non-finite or non-positive values
* fall back to the default. Also hard-clamps to just under V8's max string
* length so a misconfigured large value can never permit an over-string-limit
* buffer to reach `.toString()`.
*/
function clampMaxBufferBytes(value) {
	if (value == null || !Number.isFinite(value) || value <= 0) return DEFAULT_MAX_PROXY_BUFFER_BYTES;
	return Math.min(value, PROXY_BUFFER_HARD_CEILING);
}
/**
* Sanitize the configured proxy-buffer frame cap: non-finite or non-positive
* values fall back to the default. Bounds the count-indexed per-frame state
* (`frameTimestamps` + parse buffers) that the byte cap cannot bound on its own.
*/
function clampMaxBufferFrames(value) {
	if (value == null || !Number.isFinite(value) || value <= 0) return DEFAULT_MAX_PROXY_BUFFER_FRAMES;
	return Math.floor(value);
}
function makeUpstreamRequest(target, headers, body, clientRes, method = "POST", logger, timeouts, maxBufferBytes = DEFAULT_MAX_PROXY_BUFFER_BYTES, maxBufferFrames = DEFAULT_MAX_PROXY_BUFFER_FRAMES) {
	return new Promise((resolve, reject) => {
		const transport = target.protocol === "https:" ? node_https : node_http;
		const UPSTREAM_TIMEOUT_MS = clampTimeout(timeouts?.upstreamTimeoutMs, 3e4);
		const BODY_TIMEOUT_MS = clampTimeout(timeouts?.bodyTimeoutMs, 3e4);
		let requestSendTime = 0;
		const req = transport.request(target, {
			method,
			timeout: UPSTREAM_TIMEOUT_MS,
			headers: {
				...headers,
				"Content-Length": Buffer.byteLength(body).toString()
			}
		}, (res) => {
			res.setTimeout(BODY_TIMEOUT_MS, () => {
				req.destroy(/* @__PURE__ */ new Error(`Upstream response timed out after ${BODY_TIMEOUT_MS / 1e3}s`));
			});
			const ct = res.headers["content-type"];
			const ctStr = pickContentType(ct);
			const ctLower = ctStr.toLowerCase();
			const isSSE = ctLower.includes("text/event-stream");
			const isNDJSON = ctLower.includes("application/x-ndjson");
			const isBinaryEventStream = ctLower.includes("application/vnd.amazon.eventstream");
			const isProgressiveStream = isSSE || isNDJSON || isBinaryEventStream;
			const frameTimestamps = [];
			const streamStartTime = requestSendTime || Date.now();
			let frameBuffer = "";
			const frameDecoder = new StreamingFrameDecoder();
			let binaryFrameBuffer = Buffer.alloc(0);
			let streamedToClient = false;
			let clientDisconnected = false;
			let sawDone = false;
			if (isProgressiveStream && clientRes && !clientRes.headersSent) {
				const relayHeaders = {
					"Cache-Control": "no-cache, no-transform",
					Connection: "keep-alive",
					"X-Accel-Buffering": "no"
				};
				if (ctStr) relayHeaders["Content-Type"] = ctStr;
				const rawStatus = res.statusCode ?? 200;
				const clientStatus = rawStatus >= 200 && rawStatus < 300 ? 200 : 502;
				clientRes.writeHead(clientStatus, relayHeaders);
				if (typeof clientRes.flushHeaders === "function") clientRes.flushHeaders();
				streamedToClient = true;
				clientRes.on("close", () => {
					if (!clientRes.writableFinished) {
						clientDisconnected = true;
						if (!sawDone) {
							req.destroy();
							res.removeListener("data", onUpstreamData);
							chunks.length = 0;
							bufferedBytes = 0;
							frameTimestamps.length = 0;
							frameBuffer = "";
							binaryFrameBuffer = Buffer.alloc(0);
						}
					}
				});
			}
			const chunks = [];
			let bufferedBytes = 0;
			let totalBytes = 0;
			let bufferTruncated = false;
			/** Which cap tripped truncation — surfaced to the caller for an accurate warning. */
			let truncationCap;
			const hardCeiling = effectiveHardCeiling();
			/**
			* True when a NON-progressive upstream body exceeded `hardCeiling`, so
			* the buffered `rawBuffer` is a PARTIAL copy that must NOT be relayed as
			* success. Distinct from `bufferTruncated` (which also covers the
			* under-ceiling over-soft-cap case where the full body IS retained).
			*/
			let hardCeilingExceeded = false;
			const tripTruncation = (cap) => {
				bufferTruncated = true;
				truncationCap = cap;
				frameTimestamps.length = 0;
				frameBuffer = "";
				frameDecoder.end();
				binaryFrameBuffer = Buffer.alloc(0);
				if (isProgressiveStream) {
					chunks.length = 0;
					bufferedBytes = 0;
				}
				const detail = cap === "byte" ? `byte cap (${maxBufferBytes} bytes)` : `frame cap (${maxBufferFrames} frames)`;
				logger?.warn(`Upstream response exceeded the proxy buffer ${detail} — relaying full body to client, but skipping in-memory collapse/recording to bound memory`);
			};
			const onUpstreamData = (chunk) => {
				totalBytes += chunk.length;
				if (!bufferTruncated && bufferedBytes + chunk.length > maxBufferBytes) tripTruncation("byte");
				else if (!bufferTruncated && frameTimestamps.length >= maxBufferFrames) tripTruncation("frame");
				if (!bufferTruncated) {
					chunks.push(chunk);
					bufferedBytes += chunk.length;
				} else if (!isProgressiveStream) if (bufferedBytes + chunk.length <= hardCeiling) {
					chunks.push(chunk);
					bufferedBytes += chunk.length;
				} else hardCeilingExceeded = true;
				if (!bufferTruncated && (isSSE || isNDJSON)) {
					frameBuffer += frameDecoder.write(chunk);
					const delimiter = isNDJSON ? /\r?\n/ : /\r?\n\r?\n/;
					const parts = frameBuffer.split(delimiter);
					for (let fi = 0; fi < parts.length - 1; fi++) {
						if (frameTimestamps.length >= maxBufferFrames) {
							tripTruncation("frame");
							break;
						}
						const frame = parts[fi].trim();
						if (frame.length > 0) {
							frameTimestamps.push(Date.now());
							if (frame === "data: [DONE]") sawDone = true;
						}
					}
					if (!bufferTruncated) frameBuffer = parts[parts.length - 1];
				}
				if (!bufferTruncated && isBinaryEventStream) if (bufferedBytes + binaryFrameBuffer.length > maxBufferBytes) tripTruncation("byte");
				else {
					binaryFrameBuffer = Buffer.concat([binaryFrameBuffer, chunk]);
					while (binaryFrameBuffer.length >= 4) {
						if (frameTimestamps.length >= maxBufferFrames) {
							tripTruncation("frame");
							break;
						}
						const totalLen = binaryFrameBuffer.readUInt32BE(0);
						if (totalLen < 12 || binaryFrameBuffer.length < totalLen) break;
						frameTimestamps.push(Date.now());
						binaryFrameBuffer = binaryFrameBuffer.subarray(totalLen);
					}
				}
				if (streamedToClient && clientRes && !clientDisconnected && !clientRes.destroyed && !clientRes.writableEnded) try {
					clientRes.write(chunk);
				} catch (writeErr) {
					logger?.debug(`Failed to relay chunk to client: ${writeErr instanceof Error ? writeErr.message : "unknown"}`);
					clientDisconnected = true;
				}
			};
			res.on("data", onUpstreamData);
			res.on("error", reject);
			res.on("end", () => {
				if (res.socket) res.setTimeout(0);
				if (!bufferTruncated && (isSSE || isNDJSON)) {
					frameBuffer += frameDecoder.end();
					if (frameBuffer.trim().length > 0) frameTimestamps.push(Date.now());
				}
				const rawBuffer = Buffer.concat(chunks);
				if (streamedToClient && clientRes && !clientDisconnected && !clientRes.destroyed && !clientRes.writableEnded) try {
					clientRes.end();
				} catch (endErr) {
					logger?.debug(`Failed to end client response: ${endErr instanceof Error ? endErr.message : "unknown"}`);
				}
				const bodyString = !bufferTruncated || bufferTruncated && !streamedToClient ? rawBuffer.toString() : "";
				resolve({
					status: res.statusCode ?? 500,
					headers: res.headers,
					body: bodyString,
					rawBuffer,
					streamedToClient,
					clientDisconnected,
					frameTimestamps,
					streamStartTime,
					bufferTruncated,
					truncationCap,
					totalBytes,
					hardCeilingExceeded,
					sawDone
				});
			});
		});
		req.on("timeout", () => {
			req.destroy(/* @__PURE__ */ new Error(`Upstream request timed out after ${UPSTREAM_TIMEOUT_MS / 1e3}s: ${target.href}`));
		});
		req.on("error", reject);
		requestSendTime = Date.now();
		req.write(body);
		req.end();
	});
}
/**
* A captured Anthropic thinking-block signature is only persisted alongside
* non-empty plaintext `reasoning` (a bare signature has nothing to attach to on
* replay). When that gate drops a present signature, warn so the loss is
* observable (matching the recording-side anomaly convention in this file).
*/
function logDroppedReasoningSignature(logger, reasoning, reasoningSignature) {
	if (reasoningSignature && !reasoning) logger?.warn("Dropping captured reasoningSignature — no plaintext reasoning to attach it to");
}
/**
* Coerce a tool call's `arguments` field into the string the fixture contract
* requires (types.ts `ToolCall.arguments: string`). When the upstream omits the
* arguments field entirely, `JSON.stringify(undefined)` yields the JS value
* `undefined` (not a string), which then gets dropped on disk-write and breaks
* cross-provider replay through the OpenAI streaming path. Coalesce to "{}" so
* `arguments` is ALWAYS a string, matching the streaming recorder's behavior.
*/
function toToolCallArguments(raw) {
	if (typeof raw === "string") return raw;
	if (raw === void 0 || raw === null) return "{}";
	return JSON.stringify(raw);
}
/**
* Detect the response format from the parsed upstream JSON and convert
* it into an aimock FixtureResponse.
*/
function buildFixtureResponse(parsed, status, logger) {
	if (parsed === null || parsed === void 0) return {
		error: {
			message: "Upstream returned non-JSON response",
			type: "proxy_error"
		},
		status
	};
	const obj = parsed;
	if (typeof obj.error === "object" && obj.error !== null && typeof obj.error.message === "string") {
		const err = obj.error;
		return {
			error: {
				message: String(err.message ?? "Unknown error"),
				type: String(err.type ?? "api_error"),
				code: err.code ? String(err.code) : void 0
			},
			status
		};
	}
	if (Array.isArray(obj.data) && obj.data.length > 0) {
		const first = obj.data[0];
		if (Array.isArray(first.embedding)) return { embedding: first.embedding };
		if (typeof first.embedding === "string") {
			const buf = Buffer.from(first.embedding, "base64");
			if (buf.byteLength === 0 || buf.byteLength % 4 !== 0) logger?.warn(`Could not decode base64 embedding (byteLength=${buf.byteLength} is not a positive multiple of 4) — saving as error fixture`);
			else {
				const copied = new Uint8Array(buf);
				const floats = new Float32Array(copied.buffer, 0, buf.byteLength / 4);
				return { embedding: Array.from(floats) };
			}
		}
		if (obj.data.some((item) => item.url || item.b64_json)) {
			const dataItems = obj.data;
			const images = dataItems.filter((item) => item.url || item.b64_json).map((item) => ({
				...item.url ? { url: String(item.url) } : {},
				...item.b64_json ? { b64Json: String(item.b64_json) } : {},
				...item.revised_prompt ? { revisedPrompt: String(item.revised_prompt) } : {}
			}));
			const droppedCount = dataItems.length - images.length;
			if (droppedCount > 0) logger?.warn(`Dropped ${droppedCount} image item(s) from batch lacking a non-empty url or b64_json`);
			if (images.length === 1) return { image: images[0] };
			return { images };
		}
	}
	if (Array.isArray(obj.predictions)) {
		const images = obj.predictions.map((p) => ({
			...p.bytesBase64Encoded ? { b64Json: String(p.bytesBase64Encoded) } : {},
			...p.mimeType ? { mimeType: String(p.mimeType) } : {}
		}));
		if (images.length === 1) return { image: images[0] };
		return { images };
	}
	if (typeof obj.text === "string" && (obj.task === "transcribe" || obj.language !== void 0 && obj.duration !== void 0) && !("choices" in obj) && !("candidates" in obj) && !("object" in obj) && !("message" in obj) && !("outputs" in obj)) return { transcription: {
		text: obj.text,
		...obj.language ? { language: String(obj.language) } : {},
		...obj.duration !== void 0 ? { duration: Number(obj.duration) } : {},
		...Array.isArray(obj.words) ? { words: obj.words } : {},
		...Array.isArray(obj.segments) ? { segments: obj.segments } : {}
	} };
	if (Array.isArray(obj.outputs) && obj.outputs.length > 0 && !("choices" in obj) && !("content" in obj) && !("candidates" in obj)) {
		const outputs = obj.outputs;
		const fnCallOutputs = outputs.filter((o) => o.type === "function_call");
		const textOutputs = outputs.filter((o) => o.type === "text" && typeof o.text === "string");
		const hasToolCalls = fnCallOutputs.length > 0;
		const joinedText = textOutputs.map((o) => String(o.text ?? "")).join("");
		const hasContent = joinedText.length > 0;
		if (hasToolCalls) {
			const toolCalls = fnCallOutputs.map((o) => ({
				name: String(o.name),
				arguments: toToolCallArguments(o.arguments),
				...o.id ? { id: String(o.id) } : {}
			}));
			if (hasContent) return {
				content: joinedText,
				toolCalls
			};
			return { toolCalls };
		}
		if (hasContent) return { content: joinedText };
		return { content: "" };
	}
	if (typeof obj.id === "string" && typeof obj.status === "string" && (obj.status === "completed" || obj.status === "in_progress" || obj.status === "failed") && !("choices" in obj) && !("content" in obj) && !("candidates" in obj) && !("message" in obj) && !("data" in obj) && !("object" in obj) && !("outputs" in obj) && !("model" in obj) && !("response" in obj) && !("done" in obj) && !("usage" in obj) && !("error" in obj)) {
		if (obj.status === "completed" && obj.url) return { video: {
			id: String(obj.id),
			status: "completed",
			url: String(obj.url)
		} };
		return { video: {
			id: String(obj.id),
			status: obj.status === "failed" ? "failed" : "processing"
		} };
	}
	if (Array.isArray(obj.embedding)) return { embedding: obj.embedding };
	if (Array.isArray(obj.choices) && obj.choices.length > 0) {
		const message = obj.choices[0].message;
		if (message) {
			const hasToolCalls = Array.isArray(message.tool_calls) && message.tool_calls.length > 0;
			const hasContent = typeof message.content === "string" && message.content.length > 0;
			const openaiReasoning = typeof message.reasoning_content === "string" && message.reasoning_content.length > 0 ? message.reasoning_content : typeof message.reasoning === "string" && message.reasoning.length > 0 ? message.reasoning : void 0;
			if (hasToolCalls) {
				const toolCalls = message.tool_calls.map((tc) => {
					const fn = tc.function;
					return {
						name: String(fn.name),
						arguments: toToolCallArguments(fn.arguments),
						...tc.id ? { id: String(tc.id) } : {}
					};
				});
				if (hasContent) return {
					content: message.content,
					toolCalls,
					...openaiReasoning ? { reasoning: openaiReasoning } : {}
				};
				return {
					toolCalls,
					...openaiReasoning ? { reasoning: openaiReasoning } : {}
				};
			}
			if (hasContent) return {
				content: message.content,
				...openaiReasoning ? { reasoning: openaiReasoning } : {}
			};
			return {
				content: "",
				...openaiReasoning ? { reasoning: openaiReasoning } : {}
			};
		}
	}
	if (Array.isArray(obj.content) && obj.content.length > 0) {
		const blocks = obj.content;
		const toolUseBlocks = blocks.filter((b) => b.type === "tool_use");
		const textBlocks = blocks.filter((b) => b.type === "text" && typeof b.text === "string");
		const thinkingBlocks = blocks.filter((b) => b.type === "thinking");
		const redactedBlocks = blocks.filter((b) => b.type === "redacted_thinking");
		const redactedThinking = blocks.map((b) => require_stream_collapse.capturedRedactedData(b)).filter((data) => data !== void 0);
		const hasToolCalls = toolUseBlocks.length > 0;
		const joinedText = textBlocks.map((b) => String(b.text ?? "")).join("");
		const hasContent = joinedText.length > 0;
		const anthropicReasoning = thinkingBlocks.length > 0 ? thinkingBlocks.map((b) => String(b.thinking ?? "")).join("") : void 0;
		const anthropicReasoningSignature = (() => {
			let sig;
			for (const b of thinkingBlocks) if (typeof b.signature === "string") sig = String(b.signature);
			return sig;
		})();
		const reasoningSignatureSpread = anthropicReasoning && anthropicReasoningSignature ? { reasoningSignature: anthropicReasoningSignature } : {};
		logDroppedReasoningSignature(logger, anthropicReasoning, anthropicReasoningSignature);
		const redactedThinkingSpread = redactedThinking.length > 0 ? { redactedThinking } : {};
		if (hasToolCalls) {
			const toolCalls = toolUseBlocks.map((b) => ({
				name: String(b.name),
				arguments: toToolCallArguments(b.input),
				...b.id ? { id: String(b.id) } : {}
			}));
			if (hasContent) return {
				content: joinedText,
				toolCalls,
				...anthropicReasoning ? { reasoning: anthropicReasoning } : {},
				...reasoningSignatureSpread,
				...redactedThinkingSpread
			};
			return {
				toolCalls,
				...anthropicReasoning ? { reasoning: anthropicReasoning } : {},
				...reasoningSignatureSpread,
				...redactedThinkingSpread
			};
		}
		if (hasContent) return {
			content: joinedText,
			...anthropicReasoning ? { reasoning: anthropicReasoning } : {},
			...reasoningSignatureSpread,
			...redactedThinkingSpread
		};
		if (thinkingBlocks.length > 0 || redactedBlocks.length > 0) return {
			content: "",
			...anthropicReasoning ? { reasoning: anthropicReasoning } : {},
			...reasoningSignatureSpread,
			...redactedThinkingSpread
		};
	}
	if (Array.isArray(obj.candidates) && obj.candidates.length > 0) {
		const content = obj.candidates[0].content;
		if (content && Array.isArray(content.parts)) {
			const parts = content.parts;
			const audioParts = parts.filter((p) => {
				const inline = p.inlineData;
				if (inline === void 0 || inline === null || typeof inline.data !== "string" || inline.data.length === 0) return false;
				const mt = inline.mimeType;
				if (typeof mt === "string" && mt.length > 0) return mt.startsWith("audio/");
				return true;
			});
			if (audioParts.length > 0) {
				const inlineData = audioParts[0].inlineData;
				return { audio: {
					b64Json: String(inlineData.data ?? ""),
					contentType: typeof inlineData.mimeType === "string" && inlineData.mimeType.length > 0 ? inlineData.mimeType : "audio/mpeg"
				} };
			}
			const fnCallParts = parts.filter((p) => p.functionCall);
			const textParts = parts.filter((p) => typeof p.text === "string" && !p.thought);
			const thoughtParts = parts.filter((p) => p.thought === true && typeof p.text === "string");
			const hasToolCalls = fnCallParts.length > 0;
			const joinedText = textParts.map((p) => String(p.text ?? "")).join("");
			const hasContent = joinedText.length > 0;
			const geminiReasoning = thoughtParts.length > 0 ? thoughtParts.map((p) => String(p.text ?? "")).join("") : void 0;
			if (hasToolCalls) {
				const toolCalls = fnCallParts.map((p) => {
					const fc = p.functionCall;
					return {
						name: String(fc.name),
						arguments: toToolCallArguments(fc.args)
					};
				});
				if (hasContent) return {
					content: joinedText,
					toolCalls,
					...geminiReasoning ? { reasoning: geminiReasoning } : {}
				};
				return {
					toolCalls,
					...geminiReasoning ? { reasoning: geminiReasoning } : {}
				};
			}
			if (hasContent) return {
				content: joinedText,
				...geminiReasoning ? { reasoning: geminiReasoning } : {}
			};
			return {
				content: "",
				...geminiReasoning ? { reasoning: geminiReasoning } : {}
			};
		}
	}
	if (obj.output && typeof obj.output === "object") {
		const msg = obj.output.message;
		if (msg && Array.isArray(msg.content)) {
			const blocks = msg.content;
			const toolUseBlocks = blocks.filter((b) => b.toolUse);
			const textBlocks = blocks.filter((b) => typeof b.text === "string");
			const reasoningBlocks = blocks.filter((b) => b.reasoningContent);
			const hasToolCalls = toolUseBlocks.length > 0;
			const joinedText = textBlocks.map((b) => String(b.text ?? "")).join("");
			const hasContent = joinedText.length > 0;
			const bedrockReasoning = reasoningBlocks.length > 0 ? reasoningBlocks.map((b) => {
				const rt = b.reasoningContent?.reasoningText;
				return String(rt?.text ?? "");
			}).join("") : void 0;
			if (hasToolCalls) {
				const toolCalls = toolUseBlocks.map((b) => {
					const tu = b.toolUse;
					return {
						name: String(tu.name ?? ""),
						arguments: toToolCallArguments(tu.input),
						...tu.toolUseId ? { id: String(tu.toolUseId) } : {}
					};
				});
				if (hasContent) return {
					content: joinedText,
					toolCalls,
					...bedrockReasoning ? { reasoning: bedrockReasoning } : {}
				};
				return {
					toolCalls,
					...bedrockReasoning ? { reasoning: bedrockReasoning } : {}
				};
			}
			if (hasContent) return {
				content: joinedText,
				...bedrockReasoning ? { reasoning: bedrockReasoning } : {}
			};
			return {
				content: "",
				...bedrockReasoning ? { reasoning: bedrockReasoning } : {}
			};
		}
	}
	if (typeof obj.finish_reason === "string" && obj.message && typeof obj.message === "object" && Array.isArray(obj.message.content)) {
		const msg = obj.message;
		const contentBlocks = msg.content;
		const joinedText = contentBlocks.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => String(b.text ?? "")).join("");
		const hasContent = joinedText.length > 0;
		const toolCallBlocks = contentBlocks.filter((b) => b.type === "tool_call");
		const msgToolCalls = Array.isArray(msg.tool_calls) ? msg.tool_calls : [];
		if (toolCallBlocks.length > 0) {
			const toolCalls = toolCallBlocks.map((b) => ({
				name: String(b.name ?? b.function?.name ?? ""),
				arguments: typeof b.parameters === "string" ? b.parameters : typeof b.parameters === "object" ? JSON.stringify(b.parameters) : typeof b.function?.arguments === "string" ? String(b.function.arguments) : toToolCallArguments(b.function?.arguments),
				...b.id ? { id: String(b.id) } : {}
			}));
			if (hasContent) return {
				content: joinedText,
				toolCalls
			};
			return { toolCalls };
		}
		if (msgToolCalls.length > 0) {
			const toolCalls = msgToolCalls.map((tc) => {
				const fn = tc.function;
				return {
					name: String(tc.name ?? fn?.name ?? ""),
					arguments: typeof tc.parameters === "string" ? tc.parameters : typeof tc.parameters === "object" ? JSON.stringify(tc.parameters) : typeof fn?.arguments === "string" ? String(fn.arguments) : toToolCallArguments(fn?.arguments),
					...tc.id ? { id: String(tc.id) } : {}
				};
			});
			if (hasContent) return {
				content: joinedText,
				toolCalls
			};
			return { toolCalls };
		}
		if (hasContent) return { content: joinedText };
		return { content: "" };
	}
	if (obj.message && typeof obj.message === "object") {
		const msg = obj.message;
		const hasOllamaToolCalls = Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0;
		const hasOllamaContent = typeof msg.content === "string" && msg.content.length > 0;
		if (hasOllamaToolCalls) {
			const toolCalls = msg.tool_calls.filter((tc) => tc.function != null).map((tc) => {
				const fn = tc.function;
				return {
					name: String(fn.name ?? ""),
					arguments: toToolCallArguments(fn.arguments)
				};
			});
			if (hasOllamaContent) return {
				content: msg.content,
				toolCalls
			};
			return { toolCalls };
		}
		if (hasOllamaContent) return { content: msg.content };
		if (Array.isArray(msg.content) && msg.content.length > 0) {
			const first = msg.content[0];
			if (typeof first.text === "string") return { content: first.text };
		}
	}
	if (typeof obj.response === "string" && typeof obj.done === "boolean") return { content: obj.response };
	logger?.warn(`Could not detect response format from upstream (status=${status}) — saving as error fixture; top-level keys: [${Object.keys(obj).join(", ")}]`);
	return {
		error: {
			message: "Could not detect response format from upstream",
			type: "proxy_error"
		},
		status
	};
}
function buildFixtureMatch(request, recordConfig) {
	const match = {};
	if (request._endpointType && request._endpointType !== "chat") match.endpoint = request._endpointType;
	if (request.embeddingInput) {
		match.inputText = request.embeddingInput;
		return match;
	}
	const lastUser = require_router.getLastMessageByRole(request.messages ?? [], "user");
	if (lastUser) {
		const text = require_router.getTextContent(lastUser.content);
		if (text) match.userMessage = text;
	}
	if (request.model) match.model = require_model_utils.normalizeModelName(request.model, recordConfig?.recordFullModelVersion) ?? request.model;
	const messages = request.messages ?? [];
	if (messages.length > 0 && (request._endpointType === "chat" || request._endpointType === void 0)) {
		match.turnIndex = messages.filter((m) => m.role === "assistant").length;
		match.hasToolResult = messages.some((m) => m.role === "tool");
	}
	if (request._context) match.context = request._context;
	return match;
}
/**
* Build optional metadata for drift detection. Contains 8-char SHA-256
* hashes of the system prompt and tool definitions present in the request.
* Returns undefined when neither is present.
*/
function buildFixtureMetadata(request) {
	const meta = {};
	const systemTexts = (request.messages ?? []).filter((m) => m.role === "system").map((m) => typeof m.content === "string" ? m.content : JSON.stringify(m.content)).join("\n");
	if (systemTexts) meta.systemHash = node_crypto.createHash("sha256").update(systemTexts).digest("hex").slice(0, 8);
	if (request.tools && request.tools.length > 0) meta.toolsHash = node_crypto.createHash("sha256").update(JSON.stringify(request.tools)).digest("hex").slice(0, 8);
	return Object.keys(meta).length > 0 ? meta : void 0;
}

//#endregion
exports.buildFixtureMatch = buildFixtureMatch;
exports.buildForwardHeaders = buildForwardHeaders;
exports.clampTimeout = clampTimeout;
exports.persistFixture = persistFixture;
exports.proxyAndRecord = proxyAndRecord;
exports.sanitizeHeaderValue = sanitizeHeaderValue;
//# sourceMappingURL=recorder.cjs.map