import { DEFAULT_TEST_ID } from "./constants.js";
import { isReasoningModel } from "./model-utils.js";
import { createHash, randomBytes } from "node:crypto";

//#region src/helpers.ts
const REDACTED_HEADERS = new Set([
	"authorization",
	"x-api-key",
	"api-key"
]);
/**
* Resolve effective strict mode from per-request header and server default.
* Header values override the server default — same precedence pattern as chaos
* config headers (see resolveChaosConfig in chaos.ts).
*
* Header: `X-AIMock-Strict` — "true"/"1" → strict on, "false"/"0" → strict off.
* When absent or unrecognised, falls back to the server-level default.
*/
function resolveStrictMode(serverDefault, rawHeaders) {
	if (rawHeaders) {
		const header = rawHeaders["x-aimock-strict"];
		const val = typeof header === "string" ? header : Array.isArray(header) ? header[0] : void 0;
		if (val === "true" || val === "1") return true;
		if (val === "false" || val === "0") return false;
	}
	return serverDefault ?? false;
}
/**
* Returns `true` or `false` when the X-AIMock-Strict header overrides the
* server default, or `undefined` when it doesn't. Designed to be spread
* directly into a journal entry's `response` object:
*
*   response: { status, fixture, ...strictOverrideField(defaults.strict, req.headers) }
*/
function strictOverrideField(serverDefault, rawHeaders) {
	const effective = resolveStrictMode(serverDefault, rawHeaders);
	if (effective !== (serverDefault ?? false)) return { strictOverride: effective };
	return {};
}
/**
* Build the strict-mode 503 error message, distinguishing a true no-match from
* a sequence/turn-exhausted miss.
*
* `skippedBySequenceOrTurn` is the count reported by `matchFixtureDiagnostic`
* (router.ts): the number of fixtures that matched the request SHAPE but were
* rejected ONLY by their `sequenceIndex`/`turnIndex` count state.
*
*   - `0`  → `"Strict mode: no fixture matched"` (no candidate had a matching shape)
*   - `>0` → `"Strict mode: N candidate fixture(s) skipped by sequence/turn state"`
*
* The HTTP status (503) and error envelope shape are unchanged at every call
* site — only this message string differs. Endpoints with no sequence/turn
* gates always pass `0` and therefore see the generic message.
*/
function strictNoMatchMessage(skippedBySequenceOrTurn) {
	if (skippedBySequenceOrTurn > 0) return `Strict mode: ${skippedBySequenceOrTurn} candidate fixture(s) skipped by sequence/turn state`;
	return "Strict mode: no fixture matched";
}
/**
* Build the strict-mode error LOG line, mirroring {@link strictNoMatchMessage}'s
* disambiguation so the error log distinguishes the two miss kinds too.
*/
function strictNoMatchLogLine(method, url, skippedBySequenceOrTurn) {
	if (skippedBySequenceOrTurn > 0) return `STRICT: ${skippedBySequenceOrTurn} candidate fixture(s) skipped by sequence/turn state for ${method} ${url}`;
	return `STRICT: No fixture matched for ${method} ${url}`;
}
/**
* Resolve the reasoning string to actually emit for a given model.
*
* aimock synthesizes a reasoning channel whenever a fixture carries a
* `reasoning` string, regardless of the requested model. But a non-reasoning
* model (e.g. `gpt-4.1`) would emit no reasoning against the real provider, so
* replaying it is a false green (see aimock#254). This gates the emission on
* the requested model's capability:
*
*   - no fixture reasoning            → undefined (no-op, short-circuit)
*   - reasoning-capable model         → emit unchanged, no log
*   - non-reasoning model, strict OFF → `logger.warn`, still emit (preserves
*                                       current behavior)
*   - non-reasoning model, strict ON  → `logger.error`, suppress (return undefined)
*
* Capability is decided from the REQUESTED model id (what the backend was wired
* to), not any `overrides.model` echoed in the payload.
*/
function resolveReasoningForModel(reasoning, model, strict, logger) {
	if (!reasoning) return void 0;
	if (isReasoningModel(model)) return reasoning;
	if (strict) {
		logger.error(`Strict mode: fixture has a reasoning channel but model "${model}" is not reasoning-capable — suppressing reasoning emission`);
		return;
	}
	logger.warn(`Fixture has a reasoning channel but model "${model}" is not reasoning-capable — the real provider would emit no reasoning. Emitting anyway (set X-AIMock-Strict: true to suppress).`);
	return reasoning;
}
/**
* Resolve the encrypted reasoning artifacts (`reasoningSignature` and
* `redactedThinking`) to actually emit for a given model.
*
* `redacted_thinking` blocks and a thinking `signature` ARE part of the
* reasoning channel — they are just the encrypted form of it — so they must be
* gated on the same model-capability resolution as the plaintext `reasoning`
* string (see resolveReasoningForModel). Gating only the plaintext channel
* leaves a half-gated reasoning path: replaying a fixture recorded from a
* reasoning model against a non-reasoning model would strip the `thinking`
* block but still emit `redacted_thinking` blocks, which the real provider for
* that model would never produce.
*
* Capability is decided from the REQUESTED model id, independently of whether a
* plaintext `reasoning` string is present — a fixture may carry only
* `redactedThinking` with no plaintext reasoning.
*
*   - reasoning-capable model         → emit both unchanged, no log
*   - non-reasoning model, no artifacts → no-op, nothing to suppress
*   - non-reasoning model, strict OFF → `logger.warn`, still emit (preserves
*                                       current behavior)
*   - non-reasoning model, strict ON  → `logger.error`, suppress both
*
* Must be invoked alongside resolveReasoningForModel with identical model/strict
* inputs so the plaintext and encrypted channels stay suppressed together.
*/
function resolveReasoningArtifactsForModel(reasoningSignature, redactedThinking, model, strict, logger) {
	if (!(reasoningSignature !== void 0 || (redactedThinking?.length ?? 0) > 0) || isReasoningModel(model)) return {
		reasoningSignature,
		redactedThinking
	};
	if (strict) {
		logger.error(`Strict mode: fixture has encrypted reasoning artifacts (redacted_thinking/signature) but model "${model}" is not reasoning-capable — suppressing reasoning emission`);
		return {};
	}
	logger.warn(`Fixture has encrypted reasoning artifacts (redacted_thinking/signature) but model "${model}" is not reasoning-capable — the real provider would emit no reasoning. Emitting anyway (set X-AIMock-Strict: true to suppress).`);
	return {
		reasoningSignature,
		redactedThinking
	};
}
function flattenHeaders(headers) {
	const flat = {};
	for (const [key, value] of Object.entries(headers)) {
		if (value === void 0) continue;
		if (REDACTED_HEADERS.has(key.toLowerCase())) flat[key] = "[REDACTED]";
		else flat[key] = Array.isArray(value) ? value.join(", ") : value;
	}
	return flat;
}
async function resolveResponse(fixture, request) {
	if (typeof fixture.response === "function") try {
		return normalizeFactoryResponse(await fixture.response(request));
	} catch (err) {
		const msg = err instanceof Error ? err.message : String(err);
		throw new Error(`Response factory threw: ${msg}`, { cause: err });
	}
	return fixture.response;
}
function normalizeFactoryResponse(raw) {
	const r = { ...raw };
	if (typeof r.content === "object" && r.content !== null) r.content = JSON.stringify(r.content);
	if (Array.isArray(r.toolCalls)) r.toolCalls = r.toolCalls.map((tc) => {
		if (typeof tc.arguments === "object" && tc.arguments !== null) return {
			...tc,
			arguments: JSON.stringify(tc.arguments)
		};
		return { ...tc };
	});
	if (Array.isArray(r.blocks)) r.blocks = r.blocks.map((block) => {
		if (block != null && block.type === "toolCall" && typeof block.arguments === "object" && block.arguments !== null) return {
			...block,
			arguments: JSON.stringify(block.arguments)
		};
		return { ...block };
	});
	return r;
}
function generateId(prefix = "chatcmpl") {
	return `${prefix}-${randomBytes(12).toString("base64url")}`;
}
function generateToolCallId() {
	return `call_${randomBytes(12).toString("base64url")}`;
}
function generateMessageId() {
	return `msg_${randomBytes(12).toString("base64url")}`;
}
function generateToolUseId() {
	return `toolu_${randomBytes(12).toString("base64url")}`;
}
function isTextResponse(r) {
	return "content" in r && typeof r.content === "string" && !("toolCalls" in r);
}
function isToolCallResponse(r) {
	return "toolCalls" in r && Array.isArray(r.toolCalls) && !("content" in r && typeof r.content === "string");
}
function isContentWithToolCallsResponse(r) {
	const o = r;
	const hasContentAndToolCalls = "content" in r && typeof o.content === "string" && "toolCalls" in r && Array.isArray(o.toolCalls);
	const hasNonEmptyBlocks = Array.isArray(o.blocks) && o.blocks.length > 0;
	return hasContentAndToolCalls || hasNonEmptyBlocks;
}
/**
* Validate and pass through the ordered `blocks` field of a combined
* content+toolCalls fixture. Used ONLY on the new block-iteration path (when a
* fixture explicitly sets `blocks`); it is NOT a legacy-order reconstructor —
* fixtures without `blocks` never reach this function and keep their unchanged
* text-first path.
*
* An EMPTY `blocks` array is treated as "no blocks" by every builder's
* streaming gate (`blocks && blocks.length > 0`), so it falls back to the
* legacy `{content, toolCalls}` path and never reaches this function — the gate
* is the single source of truth for "has blocks". This validator therefore only
* ever runs on a non-empty array.
*
* Accepts the relaxed on-disk {@link FixtureFileBlock} input shape — a
* `toolCall` block's `arguments` may be a string OR a JSON object/array — which
* makes the object-tolerance below type-visible and mirrors how
* normalizeResponse types its file-form input (see {@link FixtureFileBlock} /
* {@link FixtureFileContentWithToolCallsResponse}). The in-memory
* {@link FixtureBlock} form (string `arguments`) is a structural subtype, so
* existing callers that pass `FixtureBlock[]` continue to type-check.
*
* Returns the blocks in array order, NORMALIZED to {@link FixtureBlock}: a
* `text` block with a string `text`, or a `toolCall` block with string `name` +
* string `arguments` (object/array `arguments` is JSON.stringified) and an
* optional string `id`. The return type guarantees `arguments: string` — every
* caller relies on that. Throws on a malformed array or entry — same fail-fast
* idiom as the other fixture validators in this module (see e.g. the factory
* guard at {@link resolveResponse}).
*/
function resolveFixtureBlocks(blocks) {
	if (!Array.isArray(blocks)) throw new Error(`Invalid fixture blocks: expected an array, got ${typeof blocks}`);
	return blocks.map((block, i) => {
		if (block === null || typeof block !== "object") throw new Error(`Invalid fixture block at index ${i}: expected an object`);
		const b = block;
		if (b.type === "text") {
			if (typeof b.text !== "string") throw new Error(`Invalid fixture block at index ${i}: "text" block requires a string "text" field`);
			return {
				type: "text",
				text: b.text
			};
		} else if (b.type === "toolCall") {
			if (typeof b.name !== "string") throw new Error(`Invalid fixture block at index ${i}: "toolCall" block requires a string "name" field`);
			if (b.id !== void 0 && typeof b.id !== "string") throw new Error(`Invalid fixture block at index ${i}: "toolCall" block "id" must be a string when present`);
			if (typeof b.arguments === "object" && b.arguments !== null) return {
				...b,
				arguments: JSON.stringify(b.arguments)
			};
			if (typeof b.arguments !== "string") throw new Error(`Invalid fixture block at index ${i}: "toolCall" block requires a string or object "arguments" field`);
			return {
				...b,
				type: "toolCall",
				name: b.name,
				arguments: b.arguments
			};
		} else throw new Error(`Invalid fixture block at index ${i}: unknown type ${JSON.stringify(b.type)} (expected "text" or "toolCall")`);
	});
}
function isErrorResponse(r) {
	return "error" in r && r.error !== null && typeof r.error === "object" && "message" in r.error && typeof r.error.message === "string";
}
/**
* Serialize an ErrorResponse to JSON, stripping the internal-only `status`
* field that controls the HTTP status code but should never appear in the
* response body.  Real LLM APIs don't include it.
*/
function serializeErrorResponse(response) {
	return JSON.stringify({ error: {
		message: response.error.message,
		type: response.error.type ?? "server_error",
		param: response.error.param ?? null,
		code: response.error.code ?? null
	} });
}
function isEmbeddingResponse(r) {
	return "embedding" in r && Array.isArray(r.embedding);
}
function isImageResponse(r) {
	return "image" in r && typeof r.image === "object" && r.image != null || "images" in r && Array.isArray(r.images);
}
function isAudioResponse(r) {
	if (!("audio" in r)) return false;
	const a = r.audio;
	return typeof a === "string" || typeof a === "object" && a !== null && "b64Json" in a;
}
/**
* Map audio format shorthand to MIME content types.
* Shared between speech, ElevenLabs, and fal audio handlers.
*/
const FORMAT_TO_CONTENT_TYPE = {
	mp3: "audio/mpeg",
	opus: "audio/opus",
	aac: "audio/aac",
	flac: "audio/flac",
	wav: "audio/wav",
	pcm: "audio/pcm"
};
/**
* Resolve a format string (e.g. "mp3", "opus") to its MIME content type.
* Falls back to "application/octet-stream" for unknown formats.
*/
function formatToMime(format) {
	return FORMAT_TO_CONTENT_TYPE[format] ?? "application/octet-stream";
}
function isTranscriptionResponse(r) {
	return "transcription" in r && r.transcription != null && typeof r.transcription === "object";
}
function isVideoResponse(r) {
	return "video" in r && r.video != null && typeof r.video === "object";
}
function isJSONResponse(r) {
	return "json" in r && r.json !== void 0;
}
function extractOverrides(response) {
	const r = response;
	return {
		...r.id !== void 0 && { id: r.id },
		...r.created !== void 0 && { created: r.created },
		...r.model !== void 0 && { model: r.model },
		...r.usage !== void 0 && { usage: r.usage },
		...r.systemFingerprint !== void 0 && { systemFingerprint: r.systemFingerprint },
		...r.finishReason !== void 0 && { finishReason: r.finishReason },
		...r.role !== void 0 && { role: r.role }
	};
}
/**
* Rough token count estimation based on character length.
* Uses the ~4 characters per token heuristic common for English text.
*/
function estimateTokens(text) {
	return Math.max(1, Math.ceil(text.length / 4));
}
/**
* Estimate prompt tokens from a request's messages array.
*/
function estimatePromptTokens(messages) {
	let totalChars = 0;
	for (const msg of messages) if (typeof msg.content === "string") totalChars += msg.content.length;
	else if (Array.isArray(msg.content)) {
		for (const part of msg.content) if (part.text) totalChars += part.text.length;
	}
	return Math.max(1, Math.ceil(totalChars / 4));
}
/**
* Build usage object: use explicit overrides if provided, otherwise estimate.
*/
function resolveUsage(overrides, promptText, completionText) {
	if (overrides?.usage) {
		const u = overrides.usage;
		const prompt = u.prompt_tokens ?? u.input_tokens ?? u.promptTokenCount ?? 0;
		const completion = u.completion_tokens ?? u.output_tokens ?? u.candidatesTokenCount ?? 0;
		return {
			prompt_tokens: prompt,
			completion_tokens: completion,
			total_tokens: u.total_tokens ?? u.totalTokenCount ?? prompt + completion
		};
	}
	const prompt = estimateTokens(promptText || "x");
	const completion = estimateTokens(completionText || "x");
	return {
		prompt_tokens: prompt,
		completion_tokens: completion,
		total_tokens: prompt + completion
	};
}
/**
* Build an SSE usage chunk for streaming responses.
* OpenAI emits this as the final chunk before [DONE] when
* stream_options.include_usage is true. It has an empty choices array.
*/
function buildUsageChunk(id, model, created, usage, fingerprint) {
	return {
		id,
		object: "chat.completion.chunk",
		created,
		model,
		choices: [],
		usage,
		...fingerprint !== void 0 && { system_fingerprint: fingerprint }
	};
}
function buildTextChunks(content, model, chunkSize, reasoning, overrides) {
	const id = overrides?.id ?? generateId();
	const created = overrides?.created ?? Math.floor(Date.now() / 1e3);
	const effectiveModel = overrides?.model ?? model;
	const chunks = [];
	const fingerprint = overrides?.systemFingerprint;
	if (reasoning) for (let i = 0; i < reasoning.length; i += chunkSize) {
		const slice = reasoning.slice(i, i + chunkSize);
		chunks.push({
			id,
			object: "chat.completion.chunk",
			created,
			model: effectiveModel,
			choices: [{
				index: 0,
				delta: { reasoning_content: slice },
				logprobs: null,
				finish_reason: null
			}],
			...fingerprint !== void 0 && { system_fingerprint: fingerprint }
		});
	}
	chunks.push({
		id,
		object: "chat.completion.chunk",
		created,
		model: effectiveModel,
		choices: [{
			index: 0,
			delta: {
				role: overrides?.role ?? "assistant",
				content: ""
			},
			logprobs: null,
			finish_reason: null
		}],
		...fingerprint !== void 0 && { system_fingerprint: fingerprint }
	});
	for (let i = 0; i < content.length; i += chunkSize) {
		const slice = content.slice(i, i + chunkSize);
		chunks.push({
			id,
			object: "chat.completion.chunk",
			created,
			model: effectiveModel,
			choices: [{
				index: 0,
				delta: { content: slice },
				logprobs: null,
				finish_reason: null
			}],
			...fingerprint !== void 0 && { system_fingerprint: fingerprint }
		});
	}
	chunks.push({
		id,
		object: "chat.completion.chunk",
		created,
		model: effectiveModel,
		choices: [{
			index: 0,
			delta: {},
			logprobs: null,
			finish_reason: overrides?.finishReason ?? "stop"
		}],
		...fingerprint !== void 0 && { system_fingerprint: fingerprint }
	});
	return chunks;
}
function buildToolCallChunks(toolCalls, model, chunkSize, reasoning, overrides) {
	const id = overrides?.id ?? generateId();
	const created = overrides?.created ?? Math.floor(Date.now() / 1e3);
	const effectiveModel = overrides?.model ?? model;
	const chunks = [];
	const fingerprint = overrides?.systemFingerprint;
	if (reasoning) for (let i = 0; i < reasoning.length; i += chunkSize) {
		const slice = reasoning.slice(i, i + chunkSize);
		chunks.push({
			id,
			object: "chat.completion.chunk",
			created,
			model: effectiveModel,
			choices: [{
				index: 0,
				delta: { reasoning_content: slice },
				logprobs: null,
				finish_reason: null
			}],
			...fingerprint !== void 0 && { system_fingerprint: fingerprint }
		});
	}
	chunks.push({
		id,
		object: "chat.completion.chunk",
		created,
		model: effectiveModel,
		choices: [{
			index: 0,
			delta: {
				role: overrides?.role ?? "assistant",
				content: null
			},
			logprobs: null,
			finish_reason: null
		}],
		...fingerprint !== void 0 && { system_fingerprint: fingerprint }
	});
	for (let tcIdx = 0; tcIdx < toolCalls.length; tcIdx++) {
		const tc = toolCalls[tcIdx];
		const tcId = tc.id || generateToolCallId();
		chunks.push({
			id,
			object: "chat.completion.chunk",
			created,
			model: effectiveModel,
			choices: [{
				index: 0,
				delta: { tool_calls: [{
					index: tcIdx,
					id: tcId,
					type: "function",
					function: {
						name: tc.name,
						arguments: ""
					}
				}] },
				logprobs: null,
				finish_reason: null
			}],
			...fingerprint !== void 0 && { system_fingerprint: fingerprint }
		});
		const args = tc.arguments;
		for (let i = 0; i < args.length; i += chunkSize) {
			const slice = args.slice(i, i + chunkSize);
			chunks.push({
				id,
				object: "chat.completion.chunk",
				created,
				model: effectiveModel,
				choices: [{
					index: 0,
					delta: { tool_calls: [{
						index: tcIdx,
						function: { arguments: slice }
					}] },
					logprobs: null,
					finish_reason: null
				}],
				...fingerprint !== void 0 && { system_fingerprint: fingerprint }
			});
		}
	}
	chunks.push({
		id,
		object: "chat.completion.chunk",
		created,
		model: effectiveModel,
		choices: [{
			index: 0,
			delta: {},
			logprobs: null,
			finish_reason: overrides?.finishReason ?? "tool_calls"
		}],
		...fingerprint !== void 0 && { system_fingerprint: fingerprint }
	});
	return chunks;
}
function buildTextCompletion(content, model, reasoning, overrides, requestMessages) {
	const promptText = requestMessages ? requestMessages.map((m) => typeof m.content === "string" ? m.content : Array.isArray(m.content) ? m.content.map((p) => p.text ?? "").join("") : "").join("") : "";
	return {
		id: overrides?.id ?? generateId(),
		object: "chat.completion",
		created: overrides?.created ?? Math.floor(Date.now() / 1e3),
		model: overrides?.model ?? model,
		choices: [{
			index: 0,
			message: {
				role: overrides?.role ?? "assistant",
				content,
				refusal: null,
				...reasoning ? { reasoning_content: reasoning } : {}
			},
			logprobs: null,
			finish_reason: overrides?.finishReason ?? "stop"
		}],
		usage: resolveUsage(overrides, promptText, content),
		...overrides?.systemFingerprint !== void 0 && { system_fingerprint: overrides.systemFingerprint }
	};
}
function buildToolCallCompletion(toolCalls, model, reasoning, overrides, requestMessages) {
	const promptText = requestMessages ? requestMessages.map((m) => typeof m.content === "string" ? m.content : Array.isArray(m.content) ? m.content.map((p) => p.text ?? "").join("") : "").join("") : "";
	const completionText = toolCalls.map((tc) => tc.name + tc.arguments).join("");
	return {
		id: overrides?.id ?? generateId(),
		object: "chat.completion",
		created: overrides?.created ?? Math.floor(Date.now() / 1e3),
		model: overrides?.model ?? model,
		choices: [{
			index: 0,
			message: {
				role: overrides?.role ?? "assistant",
				content: null,
				refusal: null,
				...reasoning ? { reasoning_content: reasoning } : {},
				tool_calls: toolCalls.map((tc) => ({
					id: tc.id || generateToolCallId(),
					type: "function",
					function: {
						name: tc.name,
						arguments: tc.arguments
					}
				}))
			},
			logprobs: null,
			finish_reason: overrides?.finishReason ?? "tool_calls"
		}],
		usage: resolveUsage(overrides, promptText, completionText),
		...overrides?.systemFingerprint !== void 0 && { system_fingerprint: overrides.systemFingerprint }
	};
}
function buildContentWithToolCallsChunks(content, toolCalls, model, chunkSize, reasoning, overrides, blocks) {
	const id = overrides?.id ?? generateId();
	const created = overrides?.created ?? Math.floor(Date.now() / 1e3);
	const effectiveModel = overrides?.model ?? model;
	const chunks = [];
	const fingerprint = overrides?.systemFingerprint;
	if (blocks && blocks.length > 0) {
		const ordered = resolveFixtureBlocks(blocks);
		if (reasoning) for (let i = 0; i < reasoning.length; i += chunkSize) {
			const slice = reasoning.slice(i, i + chunkSize);
			chunks.push({
				id,
				object: "chat.completion.chunk",
				created,
				model: effectiveModel,
				choices: [{
					index: 0,
					delta: { reasoning_content: slice },
					logprobs: null,
					finish_reason: null
				}],
				...fingerprint !== void 0 && { system_fingerprint: fingerprint }
			});
		}
		chunks.push({
			id,
			object: "chat.completion.chunk",
			created,
			model: effectiveModel,
			choices: [{
				index: 0,
				delta: {
					role: overrides?.role ?? "assistant",
					content: ""
				},
				logprobs: null,
				finish_reason: null
			}],
			...fingerprint !== void 0 && { system_fingerprint: fingerprint }
		});
		let tcIdx = 0;
		for (const block of ordered) if (block.type === "text") for (let i = 0; i < block.text.length; i += chunkSize) {
			const slice = block.text.slice(i, i + chunkSize);
			chunks.push({
				id,
				object: "chat.completion.chunk",
				created,
				model: effectiveModel,
				choices: [{
					index: 0,
					delta: { content: slice },
					logprobs: null,
					finish_reason: null
				}],
				...fingerprint !== void 0 && { system_fingerprint: fingerprint }
			});
		}
		else {
			const tcId = block.id || generateToolCallId();
			chunks.push({
				id,
				object: "chat.completion.chunk",
				created,
				model: effectiveModel,
				choices: [{
					index: 0,
					delta: { tool_calls: [{
						index: tcIdx,
						id: tcId,
						type: "function",
						function: {
							name: block.name,
							arguments: ""
						}
					}] },
					logprobs: null,
					finish_reason: null
				}],
				...fingerprint !== void 0 && { system_fingerprint: fingerprint }
			});
			const args = block.arguments;
			for (let i = 0; i < args.length; i += chunkSize) {
				const slice = args.slice(i, i + chunkSize);
				chunks.push({
					id,
					object: "chat.completion.chunk",
					created,
					model: effectiveModel,
					choices: [{
						index: 0,
						delta: { tool_calls: [{
							index: tcIdx,
							function: { arguments: slice }
						}] },
						logprobs: null,
						finish_reason: null
					}],
					...fingerprint !== void 0 && { system_fingerprint: fingerprint }
				});
			}
			tcIdx++;
		}
		chunks.push({
			id,
			object: "chat.completion.chunk",
			created,
			model: effectiveModel,
			choices: [{
				index: 0,
				delta: {},
				logprobs: null,
				finish_reason: overrides?.finishReason ?? "tool_calls"
			}],
			...fingerprint !== void 0 && { system_fingerprint: fingerprint }
		});
		return chunks;
	}
	if (reasoning) for (let i = 0; i < reasoning.length; i += chunkSize) {
		const slice = reasoning.slice(i, i + chunkSize);
		chunks.push({
			id,
			object: "chat.completion.chunk",
			created,
			model: effectiveModel,
			choices: [{
				index: 0,
				delta: { reasoning_content: slice },
				logprobs: null,
				finish_reason: null
			}],
			...fingerprint !== void 0 && { system_fingerprint: fingerprint }
		});
	}
	chunks.push({
		id,
		object: "chat.completion.chunk",
		created,
		model: effectiveModel,
		choices: [{
			index: 0,
			delta: {
				role: overrides?.role ?? "assistant",
				content: ""
			},
			logprobs: null,
			finish_reason: null
		}],
		...fingerprint !== void 0 && { system_fingerprint: fingerprint }
	});
	for (let i = 0; i < content.length; i += chunkSize) {
		const slice = content.slice(i, i + chunkSize);
		chunks.push({
			id,
			object: "chat.completion.chunk",
			created,
			model: effectiveModel,
			choices: [{
				index: 0,
				delta: { content: slice },
				logprobs: null,
				finish_reason: null
			}],
			...fingerprint !== void 0 && { system_fingerprint: fingerprint }
		});
	}
	for (let tcIdx = 0; tcIdx < toolCalls.length; tcIdx++) {
		const tc = toolCalls[tcIdx];
		const tcId = tc.id || generateToolCallId();
		chunks.push({
			id,
			object: "chat.completion.chunk",
			created,
			model: effectiveModel,
			choices: [{
				index: 0,
				delta: { tool_calls: [{
					index: tcIdx,
					id: tcId,
					type: "function",
					function: {
						name: tc.name,
						arguments: ""
					}
				}] },
				logprobs: null,
				finish_reason: null
			}],
			...fingerprint !== void 0 && { system_fingerprint: fingerprint }
		});
		const args = tc.arguments;
		for (let i = 0; i < args.length; i += chunkSize) {
			const slice = args.slice(i, i + chunkSize);
			chunks.push({
				id,
				object: "chat.completion.chunk",
				created,
				model: effectiveModel,
				choices: [{
					index: 0,
					delta: { tool_calls: [{
						index: tcIdx,
						function: { arguments: slice }
					}] },
					logprobs: null,
					finish_reason: null
				}],
				...fingerprint !== void 0 && { system_fingerprint: fingerprint }
			});
		}
	}
	chunks.push({
		id,
		object: "chat.completion.chunk",
		created,
		model: effectiveModel,
		choices: [{
			index: 0,
			delta: {},
			logprobs: null,
			finish_reason: overrides?.finishReason ?? "tool_calls"
		}],
		...fingerprint !== void 0 && { system_fingerprint: fingerprint }
	});
	return chunks;
}
function buildContentWithToolCallsCompletion(content, toolCalls, model, reasoning, overrides, requestMessages) {
	const promptText = requestMessages ? requestMessages.map((m) => typeof m.content === "string" ? m.content : Array.isArray(m.content) ? m.content.map((p) => p.text ?? "").join("") : "").join("") : "";
	const completionText = content + toolCalls.map((tc) => tc.name + tc.arguments).join("");
	return {
		id: overrides?.id ?? generateId(),
		object: "chat.completion",
		created: overrides?.created ?? Math.floor(Date.now() / 1e3),
		model: overrides?.model ?? model,
		choices: [{
			index: 0,
			message: {
				role: overrides?.role ?? "assistant",
				content,
				refusal: null,
				...reasoning ? { reasoning_content: reasoning } : {},
				tool_calls: toolCalls.map((tc) => ({
					id: tc.id || generateToolCallId(),
					type: "function",
					function: {
						name: tc.name,
						arguments: tc.arguments
					}
				}))
			},
			logprobs: null,
			finish_reason: overrides?.finishReason ?? "tool_calls"
		}],
		usage: resolveUsage(overrides, promptText, completionText),
		...overrides?.systemFingerprint !== void 0 && { system_fingerprint: overrides.systemFingerprint }
	};
}
const DEFAULT_MAX_BODY_BYTES = 10 * 1024 * 1024;
function readBody(req, maxBytes = DEFAULT_MAX_BODY_BYTES) {
	return new Promise((resolve, reject) => {
		const chunks = [];
		let totalBytes = 0;
		let settled = false;
		req.on("data", (chunk) => {
			if (settled) return;
			totalBytes += chunk.length;
			if (totalBytes > maxBytes) {
				settled = true;
				req.destroy();
				reject(/* @__PURE__ */ new Error(`Request body exceeded size limit of ${maxBytes} bytes`));
				return;
			}
			chunks.push(chunk);
		});
		req.on("end", () => {
			if (!settled) {
				settled = true;
				resolve(Buffer.concat(chunks).toString());
			}
		});
		req.on("error", (err) => {
			if (!settled) {
				settled = true;
				reject(err);
			}
		});
	});
}
/**
* Case-insensitive substring/regex match used for search, rerank, and
* moderation endpoints where exact casing rarely matters. String patterns
* are lowercased on both sides before comparison.
*
* Note: This intentionally differs from the case-sensitive matching in
* {@link matchFixture} (router.ts), where fixture authors expect exact
* string matching against chat completion user messages.
*/
function matchesPattern(text, pattern) {
	if (typeof pattern === "string") return text.toLowerCase().includes(pattern.toLowerCase());
	const savedLastIndex = pattern.lastIndex;
	pattern.lastIndex = 0;
	const result = pattern.test(text);
	pattern.lastIndex = savedLastIndex;
	return result;
}
function getTestId(req) {
	const headerValue = req.headers["x-test-id"];
	if (Array.isArray(headerValue)) {
		if (headerValue.length > 0 && headerValue[0]) return headerValue[0];
	} else if (typeof headerValue === "string" && headerValue) return headerValue;
	const url = req.url ?? "/";
	const qIdx = url.indexOf("?");
	if (qIdx !== -1) {
		const queryValue = new URLSearchParams(url.slice(qIdx + 1)).get("testId");
		if (queryValue) return queryValue;
	}
	return DEFAULT_TEST_ID;
}
function getContext(req) {
	const headerValue = req.headers["x-aimock-context"];
	if (Array.isArray(headerValue)) {
		if (headerValue.length > 0 && headerValue[0]) return headerValue[0];
	} else if (typeof headerValue === "string" && headerValue) return headerValue;
}
/**
* Convert a test ID (e.g. Playwright titlePath) into a filesystem-safe slug
* suitable for use as a directory name in snapshot-style recording.
*/
function slugifyTestId(testId) {
	return testId.replace(/^.*?\.(?:spec|test|e2e)\.(?:tsx|ts|jsx|js|mjs|cjs)(?=\s|›|$)\s*›?\s*/i, "").replace(/\s*[›>]\s*/g, "--").replace(/[^\w-]/g, "-").replace(/-{3,}/g, "--").replace(/^-+|-+$/g, "").toLowerCase();
}
/**
* Make a request context (the `X-AIMock-Context` header value) safe to use as
* a single directory segment in a recorded-fixture path. The header is
* attacker-controllable, so a raw value containing `../`, path separators, or
* an absolute-path prefix would let the written fixture escape the configured
* fixtures base directory. Mirrors `slugifyTestId`: non-word characters
* (including `/`, `\`, and `.`) collapse to dashes, so the result is always a
* single flat segment with no traversal semantics. Returns "" when the value
* sanitizes to nothing (caller treats that as "no context segment").
*/
function slugifyContext(context) {
	return context.replace(/[^\w-]/g, "-").replace(/-{3,}/g, "--").replace(/^-+|-+$/g, "").toLowerCase();
}
const DEFAULT_EMBEDDING_DIMENSIONS = 1536;
/**
* Generate a deterministic embedding vector from input text.
* Hashes the input with SHA-256 and spreads the hash bytes across
* the requested number of dimensions, producing values in [-1, 1].
*/
function generateDeterministicEmbedding(input, dimensions = DEFAULT_EMBEDDING_DIMENSIONS) {
	let currentHash = createHash("sha256").update(input).digest();
	const embedding = new Array(dimensions);
	for (let i = 0; i < dimensions; i++) {
		if (i > 0 && i % 32 === 0) currentHash = createHash("sha256").update(currentHash).digest();
		embedding[i] = currentHash[i % 32] / 127.5 - 1;
	}
	return embedding;
}
/**
* Build an OpenAI-format embeddings API response for one or more inputs.
*/
function buildEmbeddingResponse(embeddings, model, usage) {
	return {
		object: "list",
		data: embeddings.map((embedding, index) => ({
			object: "embedding",
			index,
			embedding
		})),
		model,
		usage: {
			prompt_tokens: usage?.prompt_tokens ?? 0,
			total_tokens: usage?.total_tokens ?? 0
		}
	};
}

//#endregion
export { FORMAT_TO_CONTENT_TYPE, buildContentWithToolCallsChunks, buildContentWithToolCallsCompletion, buildEmbeddingResponse, buildTextChunks, buildTextCompletion, buildToolCallChunks, buildToolCallCompletion, buildUsageChunk, estimatePromptTokens, estimateTokens, extractOverrides, flattenHeaders, formatToMime, generateDeterministicEmbedding, generateId, generateMessageId, generateToolCallId, generateToolUseId, getContext, getTestId, isAudioResponse, isContentWithToolCallsResponse, isEmbeddingResponse, isErrorResponse, isImageResponse, isJSONResponse, isTextResponse, isToolCallResponse, isTranscriptionResponse, isVideoResponse, matchesPattern, readBody, resolveFixtureBlocks, resolveReasoningArtifactsForModel, resolveReasoningForModel, resolveResponse, resolveStrictMode, serializeErrorResponse, slugifyContext, slugifyTestId, strictNoMatchLogLine, strictNoMatchMessage, strictOverrideField };
//# sourceMappingURL=helpers.js.map