import { isHarmonyContent, parseHarmonyContent } from "./harmony.js";
import { crc32 } from "node:zlib";

//#region src/stream-collapse.ts
/**
* Stream collapsing functions for record-and-replay.
*
* Each function takes a raw streaming response body (SSE, NDJSON, or binary
* EventStream) and collapses it into a non-streaming fixture response
* containing `{ content }`, `{ toolCalls }`, or both when the stream includes
* text followed by tool calls.
*/
/**
* Normalize a tool call's accumulated `arguments` into valid JSON exactly like
* the flat-`toolCalls` recorder path: an empty / whitespace-only / missing
* value becomes `"{}"`, never `""`. Mirrors `recorder.ts` `toToolCallArguments`
* so a `blocks[].arguments` value is always parseable JSON and never disagrees
* with the flat `toolCalls` entry for the same call.
*/
function normalizeToolArguments(args) {
	if (args === void 0 || args.trim() === "") return "{}";
	return args;
}
/**
* Build a finalized {@link FixtureBlock.toolCall} from a tool-call accumulator,
* normalizing `arguments` so the block agrees byte-for-byte with the flat
* `toolCalls` entry built from the SAME accumulator object.
*/
function toToolCallBlock(ref) {
	return {
		type: "toolCall",
		name: ref.name,
		arguments: normalizeToolArguments(ref.arguments),
		...ref.id ? { id: ref.id } : {}
	};
}
/**
* Decide whether a recorded atom sequence is "interleaved" and, if so, build
* the ordered {@link FixtureBlock} list. Returns `undefined` when NOT
* interleaved (text-first, text-only, or tool-only) so callers leave
* `CollapseResult.blocks` unset and the recorder keeps the legacy shape.
*
* Interleaved ⇔ (a tool atom appears strictly before the first text atom) OR
* (a text atom appears after any tool atom). A stream with no tool atoms, or
* with no text atoms, is never interleaved. Text-first-then-tools is the common
* legacy case and is explicitly NOT interleaved.
*
* CONSISTENCY (#274): each toolCall block is derived from the SAME accumulator
* object referenced by its atom and normalized identically to the flat
* `toolCalls` path ({@link toToolCallBlock} / {@link normalizeToolArguments}).
* Because the atom `ref` is the very object the flat list is built from, the
* block and its flat counterpart describe the same call by identity — even when
* upstream tool-call indices do not match stream-arrival order. Empty/missing
* arguments normalize to `"{}"` in BOTH representations, never `""`.
*/
function buildOrderedBlocks(atoms) {
	let firstTextIndex = -1;
	let firstToolIndex = -1;
	let textAfterTool = false;
	let sawTool = false;
	let sawText = false;
	for (let i = 0; i < atoms.length; i++) if (atoms[i].kind === "text") {
		sawText = true;
		if (firstTextIndex === -1) firstTextIndex = i;
		if (sawTool) textAfterTool = true;
	} else {
		sawTool = true;
		if (firstToolIndex === -1) firstToolIndex = i;
	}
	if (!sawTool || !sawText) return void 0;
	if (!(firstToolIndex < firstTextIndex) && !textAfterTool) return void 0;
	const blocks = [];
	let pendingText = "";
	let hasPendingText = false;
	const flushText = () => {
		if (hasPendingText) {
			blocks.push({
				type: "text",
				text: pendingText
			});
			pendingText = "";
			hasPendingText = false;
		}
	};
	for (const a of atoms) if (a.kind === "text") {
		pendingText += a.text;
		hasPendingText = true;
	} else {
		flushText();
		blocks.push(toToolCallBlock(a.ref));
	}
	flushText();
	return blocks;
}
/**
* The opaque `data` of a non-empty Anthropic `redacted_thinking` block, or
* `undefined` if `block` is not a redacted_thinking block or carries empty/no
* data. NON-EMPTY is required: the replay-side validator rejects a leading
* empty-data redacted_thinking block, so recording `data: ""` would yield a
* fixture that 400s under strict replay. Shared by every capture site (SSE,
* Anthropic-native binary, non-streaming recorder) so the rule stays in one
* place.
*/
function capturedRedactedData(block) {
	if (block?.type === "redacted_thinking" && typeof block.data === "string" && block.data.length > 0) return block.data;
}
/**
* Slice the first `max` UTF-16 code units of `s` for a diagnostic sample,
* trimming a trailing lone high-surrogate so the resulting sample never ends on
* a lone high surrogate (i.e. never mid-surrogate-pair).
*/
function surrogateSafeSlice(s, max) {
	let out = s.slice(0, max);
	if (out.length > 0) {
		const last = out.charCodeAt(out.length - 1);
		if (last >= 55296 && last <= 56319) out = out.slice(0, -1);
	}
	return out;
}
/**
* Split a raw SSE body into per-event blocks.
*
* Events are delimited by a blank line. Real HTTP/SSE transports use CRLF
* (`\r\n`) line endings, so the inter-event delimiter is `\r\n\r\n` (which
* contains no `\n\n` substring) and each line ends with a trailing `\r`.
* Splitting on `/\r?\n\r?\n/` handles LF, CRLF, and mixed streams; per-line
* `\r` trimming happens in {@link splitSSELines}. Blank blocks are dropped.
*/
function splitSSEEvents(body) {
	return body.split(/\r?\n\r?\n/).filter((block) => block.trim().length > 0);
}
/**
* Split a single SSE event block into its lines, trimming a trailing `\r` so
* CRLF streams parse identically to LF streams.
*/
function splitSSELines(block) {
	return block.split("\n").map((line) => line.endsWith("\r") ? line.slice(0, -1) : line);
}
/**
* Extract the SSE `data` field from a single event block's lines.
*
* Per the SSE spec a single event may carry MULTIPLE `data:` lines; the field
* value is every data line's content joined with "\n". Collecting only the
* first `data:` line (e.g. via `.find`) corrupts payloads that a server split
* across lines. Callers MUST pass lines produced by {@link splitSSELines} so
* any trailing `\r` is already stripped. Returns the joined payload (with the
* leading "data:" prefix and one optional leading space stripped per line), or
* `undefined` when the block contains no `data:` line.
*/
function extractSSEData(lines) {
	const dataParts = [];
	for (const line of lines) {
		if (!line.startsWith("data:")) continue;
		let part = line.slice(5);
		if (part.startsWith(" ")) part = part.slice(1);
		dataParts.push(part);
	}
	if (dataParts.length === 0) return void 0;
	return dataParts.join("\n");
}
/**
* Collapse OpenAI Chat Completions SSE stream into a single response.
*
* Format:
*   data: {"id":"chatcmpl-123","choices":[{"delta":{"content":"Hello"}}]}\n\n
*   data: [DONE]\n\n
*/
function collapseOpenAISSE(body) {
	const lines = splitSSEEvents(body);
	let content = "";
	let reasoning = "";
	const webSearchQueries = [];
	let droppedChunks = 0;
	let firstDroppedSample;
	let harmonyUnparsed = false;
	let harmonyNote;
	const toolCallMap = /* @__PURE__ */ new Map();
	let nextSyntheticIndex = 1e6;
	const idKeyMap = /* @__PURE__ */ new Map();
	const orderAtoms = [];
	for (const line of lines) {
		const data = extractSSEData(splitSSELines(line));
		if (data === void 0) continue;
		const payload = data.trim();
		if (payload === "[DONE]") continue;
		let parsed;
		try {
			parsed = JSON.parse(payload);
		} catch (err) {
			droppedChunks++;
			if (droppedChunks === 1) firstDroppedSample = `parse failed (${err instanceof Error ? err.message : "unknown"}): ${surrogateSafeSlice(payload, 200)}`;
			continue;
		}
		if (parsed.type === "response.reasoning_summary_text.delta" && typeof parsed.delta === "string") {
			reasoning += parsed.delta;
			continue;
		}
		if (parsed.type === "response.output_item.done") {
			const item = parsed.item;
			if (item?.type === "web_search_call") {
				const action = item.action;
				if (action && typeof action.query === "string") {
					webSearchQueries.push(action.query);
					continue;
				}
			}
		}
		if (parsed.type === "response.output_text.delta" && typeof parsed.delta === "string") {
			content += parsed.delta;
			continue;
		}
		if (typeof parsed.type === "string" && parsed.type.startsWith("response.")) continue;
		const choices = parsed.choices;
		if (!choices || choices.length === 0) continue;
		const delta = choices[0].delta;
		if (!delta) continue;
		if (typeof delta.reasoning_content === "string") reasoning += delta.reasoning_content;
		if (typeof delta.content === "string") {
			content += delta.content;
			if (delta.content.length > 0) orderAtoms.push({
				kind: "text",
				text: delta.content
			});
		}
		const toolCalls = delta.tool_calls;
		if (toolCalls) for (const tc of toolCalls) {
			const fn = tc.function;
			const rawId = typeof tc.id === "string" ? tc.id : void 0;
			let index;
			if (typeof tc.index === "number") index = tc.index;
			else if (rawId !== void 0) {
				const existing = idKeyMap.get(rawId);
				if (existing !== void 0) index = existing;
				else {
					index = nextSyntheticIndex++;
					idKeyMap.set(rawId, index);
				}
			} else index = nextSyntheticIndex++;
			if (!toolCallMap.has(index)) {
				const created = {
					id: rawId ?? "",
					name: fn?.name ?? "",
					arguments: ""
				};
				toolCallMap.set(index, created);
				orderAtoms.push({
					kind: "toolCall",
					ref: created
				});
			}
			const entry = toolCallMap.get(index);
			if (fn?.name && typeof fn.name === "string" && !entry.name) entry.name = fn.name;
			if (tc.id && typeof tc.id === "string" && !entry.id) entry.id = tc.id;
			if (fn?.arguments && typeof fn.arguments === "string") entry.arguments += fn.arguments;
		}
	}
	const harmonyToolCalls = [];
	if (toolCallMap.size === 0 && isHarmonyContent(content)) {
		const parsed = parseHarmonyContent(content);
		if (parsed.failed) {
			harmonyUnparsed = true;
			harmonyNote = `harmony tokens present but unparseable; content preserved verbatim: ${surrogateSafeSlice(content, 200)}`;
		} else {
			content = parsed.content;
			if (parsed.reasoning) reasoning += parsed.reasoning;
			harmonyToolCalls.push(...parsed.toolCalls);
		}
	}
	if (toolCallMap.size > 0 || harmonyToolCalls.length > 0) {
		const blocks = buildOrderedBlocks(orderAtoms);
		const orderedToolCalls = orderAtoms.filter((a) => a.kind === "toolCall").map((a) => ({
			name: a.ref.name,
			arguments: normalizeToolArguments(a.ref.arguments),
			...a.ref.id ? { id: a.ref.id } : {}
		}));
		const indexSortedToolCalls = Array.from(toolCallMap.entries()).sort(([a], [b]) => a - b).map(([, tc]) => ({
			name: tc.name,
			arguments: normalizeToolArguments(tc.arguments),
			...tc.id ? { id: tc.id } : {}
		}));
		return {
			...blocks ? { blocks } : {},
			...content ? { content } : {},
			toolCalls: [...blocks ? orderedToolCalls : indexSortedToolCalls, ...harmonyToolCalls],
			...reasoning ? { reasoning } : {},
			...webSearchQueries.length > 0 ? { webSearches: webSearchQueries } : {},
			...droppedChunks > 0 ? { droppedChunks } : {},
			...firstDroppedSample ? { firstDroppedSample } : {},
			...harmonyUnparsed ? { harmonyUnparsed: true } : {},
			...harmonyNote ? { harmonyNote } : {}
		};
	}
	return {
		content,
		...reasoning ? { reasoning } : {},
		...webSearchQueries.length > 0 ? { webSearches: webSearchQueries } : {},
		...droppedChunks > 0 ? { droppedChunks } : {},
		...firstDroppedSample ? { firstDroppedSample } : {},
		...harmonyUnparsed ? { harmonyUnparsed: true } : {},
		...harmonyNote ? { harmonyNote } : {}
	};
}
/**
* Collapse Anthropic Claude Messages SSE stream into a single response.
*
* Format:
*   event: message_start\ndata: {...}\n\n
*   event: content_block_delta\ndata: {"delta":{"type":"text_delta","text":"Hello"}}\n\n
*/
function collapseAnthropicSSE(body) {
	const blocks = splitSSEEvents(body);
	let content = "";
	let reasoning = "";
	let reasoningSignature;
	const redactedThinking = [];
	let droppedChunks = 0;
	let firstDroppedSample;
	const toolCallMap = /* @__PURE__ */ new Map();
	let nextSyntheticIndex = 1e6;
	let lastSyntheticIndex;
	const orderAtoms = [];
	for (const block of blocks) {
		const lines = splitSSELines(block);
		const eventLine = lines.find((l) => l.startsWith("event:"));
		const data = extractSSEData(lines);
		if (data === void 0) continue;
		const eventType = eventLine ? eventLine.slice(6).trim() : "";
		const payload = data.trim();
		let parsed;
		try {
			parsed = JSON.parse(payload);
		} catch (err) {
			droppedChunks++;
			if (droppedChunks === 1) firstDroppedSample = `parse failed (${err instanceof Error ? err.message : "unknown"}): ${surrogateSafeSlice(payload, 200)}`;
			continue;
		}
		if (eventType === "content_block_start") {
			const contentBlock = parsed.content_block;
			const redactedData = capturedRedactedData(contentBlock);
			if (redactedData !== void 0) redactedThinking.push(redactedData);
			if (contentBlock?.type === "tool_use") {
				let index;
				if (typeof parsed.index === "number") index = parsed.index;
				else index = nextSyntheticIndex++;
				lastSyntheticIndex = index;
				const created = {
					id: contentBlock.id ?? "",
					name: contentBlock.name ?? "",
					arguments: ""
				};
				toolCallMap.set(index, created);
				orderAtoms.push({
					kind: "toolCall",
					ref: created
				});
			}
		}
		if (eventType === "content_block_delta") {
			const delta = parsed.delta;
			if (!delta) continue;
			if (delta.type === "text_delta" && typeof delta.text === "string") {
				content += delta.text;
				if (delta.text.length > 0) orderAtoms.push({
					kind: "text",
					text: delta.text
				});
			}
			if (delta.type === "thinking_delta" && typeof delta.thinking === "string") reasoning += delta.thinking;
			if (delta.type === "signature_delta" && typeof delta.signature === "string") reasoningSignature = delta.signature;
			if (delta.type === "input_json_delta" && typeof delta.partial_json === "string") {
				const index = typeof parsed.index === "number" ? parsed.index : lastSyntheticIndex;
				const entry = index !== void 0 ? toolCallMap.get(index) : void 0;
				if (entry) entry.arguments += delta.partial_json;
				else {
					droppedChunks++;
					if (droppedChunks === 1) firstDroppedSample = `input_json_delta with no correlating tool_use start: ${surrogateSafeSlice(payload, 200)}`;
				}
			}
		}
	}
	if (toolCallMap.size > 0) {
		const orderedBlocks = buildOrderedBlocks(orderAtoms);
		const orderedToolCalls = orderAtoms.filter((a) => a.kind === "toolCall").map((a) => ({
			name: a.ref.name,
			arguments: normalizeToolArguments(a.ref.arguments),
			...a.ref.id ? { id: a.ref.id } : {}
		}));
		const indexSortedToolCalls = Array.from(toolCallMap.entries()).sort(([a], [b]) => a - b).map(([, tc]) => ({
			name: tc.name,
			arguments: normalizeToolArguments(tc.arguments),
			...tc.id ? { id: tc.id } : {}
		}));
		return {
			...orderedBlocks ? { blocks: orderedBlocks } : {},
			...content ? { content } : {},
			toolCalls: orderedBlocks ? orderedToolCalls : indexSortedToolCalls,
			...reasoning ? { reasoning } : {},
			...reasoningSignature ? { reasoningSignature } : {},
			...redactedThinking.length > 0 ? { redactedThinking } : {},
			...droppedChunks > 0 ? { droppedChunks } : {},
			...firstDroppedSample ? { firstDroppedSample } : {}
		};
	}
	return {
		content,
		...reasoning ? { reasoning } : {},
		...reasoningSignature ? { reasoningSignature } : {},
		...redactedThinking.length > 0 ? { redactedThinking } : {},
		...droppedChunks > 0 ? { droppedChunks } : {},
		...firstDroppedSample ? { firstDroppedSample } : {}
	};
}
/**
* Collapse Gemini SSE stream into a single response.
*
* Format (data-only, no event prefix, no [DONE]):
*   data: {"candidates":[{"content":{"parts":[{"text":"Hello"}]}}]}\n\n
*/
function collapseGeminiSSE(body) {
	const lines = splitSSEEvents(body);
	let content = "";
	let reasoning = "";
	let droppedChunks = 0;
	let firstDroppedSample;
	let audioB64 = "";
	let audioMimeType;
	const toolCalls = [];
	const orderAtoms = [];
	for (const line of lines) {
		const data = extractSSEData(splitSSELines(line));
		if (data === void 0) continue;
		const payload = data.trim();
		let parsed;
		try {
			parsed = JSON.parse(payload);
		} catch (err) {
			droppedChunks++;
			if (droppedChunks === 1) firstDroppedSample = `parse failed (${err instanceof Error ? err.message : "unknown"}): ${surrogateSafeSlice(payload, 200)}`;
			continue;
		}
		const candidates = parsed.candidates;
		if (!candidates || candidates.length === 0) continue;
		const candidateContent = candidates[0].content;
		if (!candidateContent) continue;
		const parts = candidateContent.parts;
		if (!parts || parts.length === 0) continue;
		for (const part of parts) if (part.functionCall) {
			const fc = part.functionCall;
			const created = {
				name: String(fc.name ?? ""),
				arguments: typeof fc.args === "string" ? fc.args : JSON.stringify(fc.args ?? {})
			};
			toolCalls.push(created);
			orderAtoms.push({
				kind: "toolCall",
				ref: created
			});
		} else if (part.inlineData && typeof part.inlineData.mimeType === "string" && part.inlineData.mimeType.startsWith("audio/")) {
			const inlineData = part.inlineData;
			if (!audioMimeType) audioMimeType = inlineData.mimeType;
			if (typeof inlineData.data === "string") audioB64 += inlineData.data;
		} else if (typeof part.text === "string") if (part.thought) reasoning += part.text;
		else {
			content += part.text;
			if (part.text.length > 0) orderAtoms.push({
				kind: "text",
				text: part.text
			});
		}
	}
	const normalizedToolCalls = toolCalls.map((tc) => ({
		...tc,
		arguments: normalizeToolArguments(tc.arguments)
	}));
	if (audioB64) return {
		audioB64,
		audioMimeType,
		...content ? { content } : {},
		...reasoning ? { reasoning } : {},
		...normalizedToolCalls.length > 0 ? { toolCalls: normalizedToolCalls } : {},
		...droppedChunks > 0 ? { droppedChunks } : {},
		...firstDroppedSample ? { firstDroppedSample } : {}
	};
	if (toolCalls.length > 0) {
		const blocks = buildOrderedBlocks(orderAtoms);
		return {
			...blocks ? { blocks } : {},
			...content ? { content } : {},
			toolCalls: normalizedToolCalls,
			...reasoning ? { reasoning } : {},
			...droppedChunks > 0 ? { droppedChunks } : {},
			...firstDroppedSample ? { firstDroppedSample } : {}
		};
	}
	return {
		content,
		...reasoning ? { reasoning } : {},
		...droppedChunks > 0 ? { droppedChunks } : {},
		...firstDroppedSample ? { firstDroppedSample } : {}
	};
}
/**
* Collapse Ollama NDJSON stream into a single response.
*
* /api/chat format:
*   {"model":"llama3","message":{"role":"assistant","content":"Hello"},"done":false}\n
*
* /api/generate format:
*   {"model":"llama3","response":"Hello","done":false}\n
*
* Open-weight gpt-oss served via Ollama streams harmony channel tokens inside
* `message.content` (just like the OpenAI SSE path), so after accumulation the
* content is run through the same fail-safe {@link parseHarmonyContent} gate to
* capture structured tool calls / reasoning instead of leaking raw tokens.
*/
function collapseOllamaNDJSON(body) {
	const lines = body.split("\n").filter((l) => l.trim().length > 0);
	let content = "";
	let reasoning = "";
	let droppedChunks = 0;
	let firstDroppedSample;
	let harmonyUnparsed = false;
	let harmonyNote;
	const toolCalls = [];
	const orderAtoms = [];
	for (const line of lines) {
		let parsed;
		try {
			parsed = JSON.parse(line.trim());
		} catch (err) {
			droppedChunks++;
			if (droppedChunks === 1) firstDroppedSample = `parse failed (${err instanceof Error ? err.message : "unknown"}): ${surrogateSafeSlice(line.trim(), 200)}`;
			continue;
		}
		const message = parsed.message;
		if (message) {
			if (typeof message.content === "string") {
				content += message.content;
				if (message.content.length > 0) orderAtoms.push({
					kind: "text",
					text: message.content
				});
			}
			if (Array.isArray(message.tool_calls)) for (const tc of message.tool_calls) {
				const fn = tc.function;
				if (fn) {
					const created = {
						name: String(fn.name ?? ""),
						arguments: typeof fn.arguments === "string" ? fn.arguments : JSON.stringify(fn.arguments ?? {})
					};
					toolCalls.push(created);
					orderAtoms.push({
						kind: "toolCall",
						ref: created
					});
				}
			}
		} else if (typeof parsed.response === "string") {
			content += parsed.response;
			if (parsed.response.length > 0) orderAtoms.push({
				kind: "text",
				text: parsed.response
			});
		}
	}
	if (toolCalls.length === 0 && isHarmonyContent(content)) {
		const parsedHarmony = parseHarmonyContent(content);
		if (parsedHarmony.failed) {
			harmonyUnparsed = true;
			harmonyNote = `harmony tokens present but unparseable; content preserved verbatim: ${surrogateSafeSlice(content, 200)}`;
		} else {
			content = parsedHarmony.content;
			if (parsedHarmony.reasoning) reasoning += parsedHarmony.reasoning;
			toolCalls.push(...parsedHarmony.toolCalls);
		}
	}
	if (toolCalls.length > 0) {
		const blocks = buildOrderedBlocks(orderAtoms);
		const normalizedToolCalls = toolCalls.map((tc) => ({
			...tc,
			arguments: normalizeToolArguments(tc.arguments)
		}));
		return {
			...blocks ? { blocks } : {},
			...content ? { content } : {},
			toolCalls: normalizedToolCalls,
			...reasoning ? { reasoning } : {},
			...droppedChunks > 0 ? { droppedChunks } : {},
			...firstDroppedSample ? { firstDroppedSample } : {},
			...harmonyUnparsed ? { harmonyUnparsed: true } : {},
			...harmonyNote ? { harmonyNote } : {}
		};
	}
	return {
		content,
		...reasoning ? { reasoning } : {},
		...droppedChunks > 0 ? { droppedChunks } : {},
		...firstDroppedSample ? { firstDroppedSample } : {},
		...harmonyUnparsed ? { harmonyUnparsed: true } : {},
		...harmonyNote ? { harmonyNote } : {}
	};
}
/**
* Collapse Cohere SSE stream into a single response.
*
* Format:
*   event: content-delta\ndata: {"type":"content-delta","delta":{"message":{"content":{"text":"Hello"}}}}\n\n
*/
function collapseCohereSSE(body) {
	const blocks = splitSSEEvents(body);
	let content = "";
	let reasoning = "";
	let droppedChunks = 0;
	let firstDroppedSample;
	const toolCallMap = /* @__PURE__ */ new Map();
	let nextSyntheticIndex = 1e6;
	let lastStartKey;
	const orderAtoms = [];
	for (const block of blocks) {
		const lines = splitSSELines(block);
		const eventLine = lines.find((l) => l.startsWith("event:"));
		const data = extractSSEData(lines);
		if (data === void 0) continue;
		const eventType = eventLine ? eventLine.slice(6).trim() : "";
		const payload = data.trim();
		let parsed;
		try {
			parsed = JSON.parse(payload);
		} catch (err) {
			droppedChunks++;
			if (droppedChunks === 1) firstDroppedSample = `parse failed (${err instanceof Error ? err.message : "unknown"}): ${surrogateSafeSlice(payload, 200)}`;
			continue;
		}
		if (eventType === "content-delta") {
			const contentObj = (parsed.delta?.message)?.content;
			if (contentObj && contentObj.type === "thinking" && typeof contentObj.thinking === "string") reasoning += contentObj.thinking;
			else if (contentObj && typeof contentObj.text === "string") {
				content += contentObj.text;
				if (contentObj.text.length > 0) orderAtoms.push({
					kind: "text",
					text: contentObj.text
				});
			}
		}
		if (eventType === "tool-call-start") {
			let index;
			if (typeof parsed.index === "number") index = parsed.index;
			else index = nextSyntheticIndex++;
			lastStartKey = index;
			const toolCalls = (parsed.delta?.message)?.tool_calls;
			if (toolCalls) {
				const fn = toolCalls.function;
				const created = {
					id: toolCalls.id ?? "",
					name: fn?.name ?? "",
					arguments: ""
				};
				toolCallMap.set(index, created);
				orderAtoms.push({
					kind: "toolCall",
					ref: created
				});
			}
		}
		if (eventType === "tool-call-delta") {
			const index = typeof parsed.index === "number" ? parsed.index : lastStartKey;
			const toolCalls = (parsed.delta?.message)?.tool_calls;
			if (toolCalls) {
				const fn = toolCalls.function;
				if (fn && typeof fn.arguments === "string") {
					const entry = index !== void 0 ? toolCallMap.get(index) : void 0;
					if (entry) entry.arguments += fn.arguments;
					else {
						droppedChunks++;
						if (droppedChunks === 1) firstDroppedSample = `tool-call-delta with no correlating start: ${surrogateSafeSlice(payload, 200)}`;
					}
				}
			}
		}
	}
	if (toolCallMap.size > 0) {
		const orderedBlocks = buildOrderedBlocks(orderAtoms);
		const orderedToolCalls = orderAtoms.filter((a) => a.kind === "toolCall").map((a) => ({
			name: a.ref.name,
			arguments: normalizeToolArguments(a.ref.arguments),
			...a.ref.id ? { id: a.ref.id } : {}
		}));
		const indexSortedToolCalls = Array.from(toolCallMap.entries()).sort(([a], [b]) => a - b).map(([, tc]) => ({
			name: tc.name,
			arguments: normalizeToolArguments(tc.arguments),
			...tc.id ? { id: tc.id } : {}
		}));
		return {
			...orderedBlocks ? { blocks: orderedBlocks } : {},
			...content ? { content } : {},
			toolCalls: orderedBlocks ? orderedToolCalls : indexSortedToolCalls,
			...reasoning ? { reasoning } : {},
			...droppedChunks > 0 ? { droppedChunks } : {},
			...firstDroppedSample ? { firstDroppedSample } : {}
		};
	}
	return {
		content,
		...reasoning ? { reasoning } : {},
		...droppedChunks > 0 ? { droppedChunks } : {},
		...firstDroppedSample ? { firstDroppedSample } : {}
	};
}
/**
* Decode AWS Event Stream binary frames and extract JSON payloads.
*
* Binary frame layout:
*   [total_length: 4B uint32-BE]
*   [headers_length: 4B uint32-BE]
*   [prelude_crc32: 4B]
*   [headers: variable]
*   [payload: variable]
*   [message_crc32: 4B]
*/
function decodeEventStreamFrames(buf) {
	const frames = [];
	let offset = 0;
	while (offset < buf.length) {
		if (offset + 12 > buf.length) break;
		const totalLength = buf.readUInt32BE(offset);
		const headersLength = buf.readUInt32BE(offset + 4);
		if (totalLength < 12 || offset + totalLength > buf.length) return {
			frames,
			truncated: true
		};
		const preludeCrc = buf.readUInt32BE(offset + 8);
		const computedPreludeCrc = crc32(buf.subarray(offset, offset + 8));
		if (preludeCrc >>> 0 !== computedPreludeCrc >>> 0) return {
			frames,
			truncated: true
		};
		const headersStart = offset + 12;
		const headersEnd = headersStart + headersLength;
		const payloadEnd = offset + totalLength - 4;
		if (headersEnd > payloadEnd || headersEnd > buf.length) return {
			frames,
			truncated: true
		};
		const headers = {};
		let hOffset = headersStart;
		let headerOverrun = false;
		while (hOffset < headersEnd) {
			if (hOffset + 1 > headersEnd) {
				headerOverrun = true;
				break;
			}
			const nameLen = buf.readUInt8(hOffset);
			hOffset += 1;
			if (hOffset + nameLen + 1 + 2 > headersEnd) {
				headerOverrun = true;
				break;
			}
			const name = buf.subarray(hOffset, hOffset + nameLen).toString("utf8");
			hOffset += nameLen;
			hOffset += 1;
			const valueLen = buf.readUInt16BE(hOffset);
			hOffset += 2;
			if (hOffset + valueLen > headersEnd) {
				headerOverrun = true;
				break;
			}
			const value = buf.subarray(hOffset, hOffset + valueLen).toString("utf8");
			hOffset += valueLen;
			headers[name] = value;
		}
		if (headerOverrun) return {
			frames,
			truncated: true
		};
		const payloadStart = headersEnd;
		const payload = buf.subarray(payloadStart, payloadEnd);
		const messageCrc = buf.readUInt32BE(offset + totalLength - 4);
		const computedMessageCrc = crc32(buf.subarray(offset, offset + totalLength - 4));
		if (messageCrc >>> 0 !== computedMessageCrc >>> 0) return {
			frames,
			truncated: true
		};
		frames.push({
			headers,
			payload
		});
		offset += totalLength;
	}
	return {
		frames,
		truncated: false
	};
}
/**
* Collapse Bedrock binary Event Stream into a single response.
*
* Each frame contains a JSON payload with event types like:
*   contentBlockDelta, contentBlockStart, etc.
*/
function collapseBedrockEventStream(body) {
	const { frames, truncated } = decodeEventStreamFrames(body);
	let content = "";
	let reasoning = "";
	let reasoningSignature;
	const redactedThinking = [];
	let droppedChunks = 0;
	let firstDroppedSample;
	const toolCallMap = /* @__PURE__ */ new Map();
	const orderAtoms = [];
	for (const frame of frames) {
		const frameStr = frame.payload.toString("utf8");
		let parsed;
		try {
			parsed = JSON.parse(frameStr);
		} catch (err) {
			droppedChunks++;
			if (droppedChunks === 1) firstDroppedSample = `parse failed (${err instanceof Error ? err.message : "unknown"}): ${surrogateSafeSlice(frameStr, 200)}`;
			continue;
		}
		if (parsed.type === "content_block_delta") {
			const delta = parsed.delta;
			if (delta?.type === "text_delta" && typeof delta.text === "string") {
				content += delta.text;
				if (delta.text.length > 0) orderAtoms.push({
					kind: "text",
					text: delta.text
				});
			}
			if (delta?.type === "thinking_delta" && typeof delta.thinking === "string") reasoning += delta.thinking;
			if (delta?.type === "signature_delta" && typeof delta.signature === "string") reasoningSignature = delta.signature;
			if (delta?.type === "input_json_delta" && typeof delta.partial_json === "string") {
				const index = parsed.index;
				const entry = index !== void 0 ? toolCallMap.get(index) : void 0;
				if (entry) entry.arguments += delta.partial_json;
				else {
					droppedChunks++;
					if (droppedChunks === 1) firstDroppedSample = `input_json_delta with no correlating tool_use start: ${surrogateSafeSlice(frameStr, 200)}`;
				}
			}
			continue;
		}
		if (parsed.type === "content_block_start") {
			const block = parsed.content_block;
			const index = parsed.index;
			const redactedData = capturedRedactedData(block);
			if (redactedData !== void 0) redactedThinking.push(redactedData);
			if (block?.type === "tool_use" && index !== void 0) {
				const created = {
					id: block.id ?? "",
					name: block.name ?? "",
					arguments: ""
				};
				toolCallMap.set(index, created);
				orderAtoms.push({
					kind: "toolCall",
					ref: created
				});
			}
			continue;
		}
		if (parsed.contentBlockStart) {
			const blockStart = parsed.contentBlockStart;
			const index = parsed.contentBlockIndex ?? blockStart.contentBlockIndex;
			const start = blockStart.start;
			if (start?.toolUse && index !== void 0) {
				const toolUse = start.toolUse;
				const created = {
					id: toolUse.toolUseId ?? "",
					name: toolUse.name ?? "",
					arguments: ""
				};
				toolCallMap.set(index, created);
				orderAtoms.push({
					kind: "toolCall",
					ref: created
				});
			}
		}
		if (parsed.contentBlockDelta) {
			const blockDelta = parsed.contentBlockDelta;
			const index = parsed.contentBlockIndex ?? blockDelta.contentBlockIndex;
			const delta = blockDelta.delta;
			if (!delta) continue;
			if (typeof delta.text === "string") {
				content += delta.text;
				if (delta.text.length > 0) orderAtoms.push({
					kind: "text",
					text: delta.text
				});
			}
			if (typeof delta.reasoningContent === "object" && delta.reasoningContent !== null) {
				const reasoningDelta = delta.reasoningContent;
				if (typeof reasoningDelta.text === "string") reasoning += reasoningDelta.text;
			}
			if (typeof delta.toolUse === "object" && delta.toolUse !== null) {
				const toolUseDelta = delta.toolUse;
				if (typeof toolUseDelta.input === "string") {
					const entry = index !== void 0 ? toolCallMap.get(index) : void 0;
					if (entry) entry.arguments += toolUseDelta.input;
					else {
						droppedChunks++;
						if (droppedChunks === 1) firstDroppedSample = `toolUse.input delta with no correlating tool_use start: ${surrogateSafeSlice(frameStr, 200)}`;
					}
				}
			}
		}
	}
	if (toolCallMap.size > 0) {
		const orderedBlocks = buildOrderedBlocks(orderAtoms);
		const orderedToolCalls = orderAtoms.filter((a) => a.kind === "toolCall").map((a) => ({
			name: a.ref.name,
			arguments: normalizeToolArguments(a.ref.arguments),
			...a.ref.id ? { id: a.ref.id } : {}
		}));
		const indexSortedToolCalls = Array.from(toolCallMap.entries()).sort(([a], [b]) => a - b).map(([, tc]) => ({
			name: tc.name,
			arguments: normalizeToolArguments(tc.arguments),
			...tc.id ? { id: tc.id } : {}
		}));
		return {
			...orderedBlocks ? { blocks: orderedBlocks } : {},
			...content ? { content } : {},
			toolCalls: orderedBlocks ? orderedToolCalls : indexSortedToolCalls,
			...reasoning ? { reasoning } : {},
			...reasoningSignature ? { reasoningSignature } : {},
			...redactedThinking.length > 0 ? { redactedThinking } : {},
			...droppedChunks > 0 ? { droppedChunks } : {},
			...firstDroppedSample ? { firstDroppedSample } : {},
			...truncated ? { truncated } : {}
		};
	}
	return {
		content,
		...reasoning ? { reasoning } : {},
		...reasoningSignature ? { reasoningSignature } : {},
		...redactedThinking.length > 0 ? { redactedThinking } : {},
		...droppedChunks > 0 ? { droppedChunks } : {},
		...firstDroppedSample ? { firstDroppedSample } : {},
		...truncated ? { truncated } : {}
	};
}
/**
* Collapse Gemini Interactions SSE stream into a single response.
*
* Handles the SDK 2.x event protocol (the "Interactions breaking changes,
* May 2026" shapes):
*   data: {"event_type":"step.start","index":1,"step":{"type":"function_call","id":"call_1","name":"fn","arguments":{}}}
*   data: {"event_type":"step.delta","index":0,"delta":{"type":"text","text":"Hello"}}
*   data: {"event_type":"step.delta","index":1,"delta":{"type":"arguments_delta","arguments":"{\"x\":1}"}}
*   data: {"event_type":"interaction.completed","interaction":{"id":"...","usage":{...}}}
*
* The legacy SDK 1.x shapes (`content.delta` with an inline `function_call`
* delta) are still accepted for backward compatibility with previously
* recorded fixtures.
*/
function collapseGeminiInteractionsSSE(body) {
	const lines = splitSSEEvents(body);
	let content = "";
	let reasoning = "";
	let droppedChunks = 0;
	let firstDroppedSample;
	const toolCalls = [];
	const stepToolCalls = /* @__PURE__ */ new Map();
	let nextSyntheticStepIndex = 1e6;
	for (const line of lines) {
		const data = extractSSEData(splitSSELines(line));
		if (data === void 0) continue;
		const payload = data.trim();
		let parsed;
		try {
			parsed = JSON.parse(payload);
		} catch (err) {
			droppedChunks++;
			if (droppedChunks === 1) firstDroppedSample = `parse failed (${err instanceof Error ? err.message : "unknown"}): ${surrogateSafeSlice(payload, 200)}`;
			continue;
		}
		const eventType = parsed.event_type;
		if (!eventType) continue;
		const index = typeof parsed.index === "number" ? parsed.index : void 0;
		if (eventType === "step.start") {
			const step = parsed.step;
			if (step && step.type === "function_call") {
				const key = index ?? nextSyntheticStepIndex++;
				stepToolCalls.set(key, {
					id: step.id ? String(step.id) : void 0,
					name: String(step.name ?? ""),
					argsObj: step.arguments,
					argsStr: ""
				});
			}
		} else if (eventType === "step.delta" || eventType === "content.delta") {
			const delta = parsed.delta;
			if (!delta) continue;
			if (delta.type === "text" && typeof delta.text === "string") content += delta.text;
			else if (delta.type === "arguments_delta") {
				const entry = index !== void 0 ? stepToolCalls.get(index) : void 0;
				if (entry) {
					if (typeof delta.arguments === "string") entry.argsStr += delta.arguments;
				} else {
					droppedChunks++;
					if (droppedChunks === 1) firstDroppedSample = `arguments_delta with no correlating step.start: ${surrogateSafeSlice(payload, 200)}`;
				}
			} else if (delta.type === "function_call") toolCalls.push({
				name: String(delta.name ?? ""),
				arguments: typeof delta.arguments === "string" ? normalizeToolArguments(delta.arguments) : JSON.stringify(delta.arguments ?? {}),
				...delta.id ? { id: String(delta.id) } : {}
			});
			else if (delta.type === "thought_summary") {
				const summaryContent = delta.content;
				if (summaryContent && typeof summaryContent.text === "string") reasoning += summaryContent.text;
				else if (typeof delta.text === "string") reasoning += delta.text;
			}
		}
	}
	for (const [, tc] of Array.from(stepToolCalls.entries()).sort(([a], [b]) => a - b)) {
		let args;
		if (tc.argsStr !== "") {
			args = tc.argsStr;
			try {
				JSON.parse(args);
			} catch {
				droppedChunks++;
				if (droppedChunks === 1) firstDroppedSample = `assembled arguments_delta not valid JSON for "${tc.name}": ${surrogateSafeSlice(args, 200)}`;
				args = "{}";
			}
		} else args = typeof tc.argsObj === "string" ? tc.argsObj : JSON.stringify(tc.argsObj ?? {});
		toolCalls.push({
			name: tc.name,
			arguments: normalizeToolArguments(args),
			...tc.id ? { id: tc.id } : {}
		});
	}
	if (toolCalls.length > 0) return {
		...content ? { content } : {},
		toolCalls,
		...reasoning ? { reasoning } : {},
		...droppedChunks > 0 ? { droppedChunks } : {},
		...firstDroppedSample ? { firstDroppedSample } : {}
	};
	return {
		content,
		...reasoning ? { reasoning } : {},
		...droppedChunks > 0 ? { droppedChunks } : {},
		...firstDroppedSample ? { firstDroppedSample } : {}
	};
}
/**
* Collapse a streaming response body into a non-streaming fixture response.
* Returns null if the content type is not a known streaming format.
* Falls back to OpenAI SSE parsing for unrecognized provider keys with text/event-stream.
*/
function collapseStreamingResponse(contentType, providerKey, body, logger) {
	const ct = contentType.toLowerCase();
	if (ct.includes("application/vnd.amazon.eventstream")) return collapseBedrockEventStream(typeof body === "string" ? Buffer.from(body, "binary") : body);
	if (ct.includes("application/x-ndjson")) return collapseOllamaNDJSON(typeof body === "string" ? body : body.toString("utf8"));
	if (ct.includes("text/event-stream")) {
		const str = typeof body === "string" ? body : body.toString("utf8");
		switch (providerKey) {
			case "openai":
			case "azure": return collapseOpenAISSE(str);
			case "anthropic": return collapseAnthropicSSE(str);
			case "gemini":
			case "vertexai": return collapseGeminiSSE(str);
			case "gemini-interactions": return collapseGeminiInteractionsSSE(str);
			case "cohere": return collapseCohereSSE(str);
			case "bedrock": return collapseAnthropicSSE(str);
			default:
				logger?.warn(`[stream-collapse] unknown SSE provider "${providerKey}", falling back to OpenAI SSE format`);
				return collapseOpenAISSE(str);
		}
	}
	return null;
}

//#endregion
export { capturedRedactedData, collapseAnthropicSSE, collapseBedrockEventStream, collapseCohereSSE, collapseGeminiInteractionsSSE, collapseGeminiSSE, collapseOllamaNDJSON, collapseOpenAISSE, collapseStreamingResponse };
//# sourceMappingURL=stream-collapse.js.map