import { n as estimateTokens } from "./utils-ngQzYzZD.js";
import { c as classifyErrorPrelude, d as matchesContextExceeded, f as matchesToolPairingError, l as errorMessage, u as isRetryableHttpStatus } from "./errors-DkR6GPJw.js";
import { n as documentBlockMarker } from "./types-BiobHM1D.js";
import { t as reconcileImageMediaType } from "./image-sniff-B7uFSNO1.js";
import { getModel } from "@earendil-works/pi-ai";
//#region src/system-prompt.ts
/**
* System-prompt boundary marker — splits a system prompt into a stable static
* prefix (cached) and a per-turn dynamic suffix (NOT cached).
*
* Why this exists: providers attach `cache_control` markers on the last block
* of the system prompt, so the cached prefix covers the entire system text.
* Any byte change anywhere — including a per-turn `<env>` rewrite — busts the
* cache for the doctrine that sits below. A literal marker in the system
* string lets providers split it into:
*
*   ┌──────────────┐ cache_control: ephemeral
*   │ STATIC half  │  — doctrine, skills catalog, tool catalog,
*   │              │    user instructions
*   ├──────────────┤  ← SYSTEM_PROMPT_BOUNDARY
*   │ DYNAMIC half │  — env, cwd, mtimes, anything per-turn
*   └──────────────┘ (no cache_control)
*
* The static prefix rides the prompt cache across turns/sessions; the dynamic
* suffix re-bills per turn. Net effect: a cwd change between turns no longer
* invalidates 4 KB of doctrine.
*
* Wire contract:
*
* - `splitSystemPrompt(s)` is pure; missing marker ⇒ entire string is static
*   (current behavior — no caller is forced to opt in).
* - `renderSystemForWire(s)` strips the marker so it never reaches the model;
*   used by every provider before the bytes hit the wire, including the
*   cache-disabled path on providers that DO support `cache_control`.
* - The marker uses underscores rather than XML/punctuation so it's
*   unambiguous when scanning prompts manually and unlikely to collide with
*   model-written content.
* - Providers handle the split internally (Anthropic emits a 2-block array;
*   OpenAI-compat splits the leading `system` message into multi-part text).
*   Callers always pass a single `string` — no API break.
*/
/**
* Literal marker inserted in a system prompt to separate cacheable doctrine
* from per-turn dynamic content. Providers split on this token.
*
* Underscored on both sides for visual distinctiveness — a stray instance in
* model-written prose is implausible. Don't change the value without shipping
* a migration; existing sessions carry the old marker in their cached
* prompts.
*/
const SYSTEM_PROMPT_BOUNDARY = "__ZIDANE_SYSTEM_PROMPT_BOUNDARY__";
/**
* Split a system prompt around the first {@link SYSTEM_PROMPT_BOUNDARY}.
*
* Splits on the FIRST occurrence — subsequent markers are folded into the
* dynamic half. This way callers can append additional `<env>` style blocks
* with extra markers without each one creating a new cache layer (Anthropic
* caps breakpoints; we use the budget elsewhere). The marker itself is
* stripped from both sides — providers attach `cache_control` directly to
* the static half's text content.
*
* A single blank line (`\n\n`) immediately adjacent to the marker on each
* side is trimmed — callers conventionally write
* `<doctrine>\n\n<MARKER>\n\n<env>` and expect the rendered wire bytes to
* read as one logical paragraph. Blank lines beyond that immediate pair
* (e.g. `\n\n\n<env>`) are preserved verbatim so callers composing their
* own spacing don't lose intentional gaps.
*
* Pure / no I/O / no allocation when the marker is absent (returns the input
* verbatim on the static side and the empty string on the dynamic side).
*/
function splitSystemPrompt(system) {
	if (system.length === 0) return {
		static: "",
		dynamic: ""
	};
	const idx = system.indexOf(SYSTEM_PROMPT_BOUNDARY);
	if (idx < 0) return {
		static: system,
		dynamic: ""
	};
	const staticPart = system.slice(0, idx);
	const dynamicPart = system.slice(idx + 33);
	return {
		static: trimTrailingBlankLine(staticPart),
		dynamic: trimLeadingBlankLine(dynamicPart)
	};
}
/**
* Compose a system prompt from a static prefix and an optional dynamic
* suffix. Inserts {@link SYSTEM_PROMPT_BOUNDARY} between them only when the
* dynamic side is non-empty — single-block prompts stay marker-free, so
* callers that never opt in pay zero overhead and providers fall back to the
* existing whole-string caching path.
*
* Spacing is `\n\n` on both sides of the marker so doctrine fragments,
* which conventionally separate sections with a blank line, read cleanly
* around the boundary.
*/
function joinSystemPrompt(staticPart, dynamicPart) {
	if (dynamicPart.length === 0) return staticPart;
	if (staticPart.length === 0) return `${SYSTEM_PROMPT_BOUNDARY}\n\n${dynamicPart}`;
	return `${staticPart}\n\n${SYSTEM_PROMPT_BOUNDARY}\n\n${dynamicPart}`;
}
/**
* Append `extra` to the STATIC half of a system prompt, preserving any
* existing dynamic suffix.
*
* Used by the agent to fold in run-stable content (skills catalog, lazy tool
* catalog) without bumping it into the dynamic half — both catalogs are
* built once per run and remain byte-stable for the duration, so they
* belong in the cached prefix.
*
* Returns a new string; the input is not mutated. When `extra` is empty,
* returns the input verbatim.
*/
function appendStaticSection(system, extra) {
	if (extra.length === 0) return system;
	const { static: staticPart, dynamic } = splitSystemPrompt(system);
	return joinSystemPrompt(staticPart.length === 0 ? extra : `${staticPart}\n\n${extra}`, dynamic);
}
/**
* Append `extra` to the DYNAMIC half of a system prompt. Inserts the boundary
* marker if the input didn't already carry one.
*
* Used by hosts (typically the TUI) to inject per-turn state — current cwd,
* IDE selection, project root — without forcing every caller to know the
* marker format. The host's `system:transform` hook rewrites the dynamic
* half each turn; the static doctrine above stays byte-stable and rides the
* cache.
*
* Returns a new string; the input is not mutated. When `extra` is empty,
* returns the input verbatim.
*/
function appendDynamicSection(system, extra) {
	if (extra.length === 0) return system;
	const { static: staticPart, dynamic } = splitSystemPrompt(system);
	return joinSystemPrompt(staticPart, dynamic.length === 0 ? extra : `${dynamic}\n\n${extra}`);
}
/**
* Replace the entire dynamic half of a system prompt with `next`. Used by
* the TUI's `<env>` rewriter where the entire dynamic section is regenerated
* each turn rather than appended to.
*
* When `next` is empty, drops the dynamic half (and the marker) entirely.
*/
function replaceDynamicSection(system, next) {
	const { static: staticPart } = splitSystemPrompt(system);
	return joinSystemPrompt(staticPart, next);
}
/**
* Strip the boundary marker so it never reaches the wire — collapses
* `<static>${BOUNDARY}<dynamic>` into `<static>\n\n<dynamic>` for providers
* that can't honor `cache_control` (vanilla OpenAI Chat Completions, Codex,
* Cerebras, ...) or for the cache-disabled path on any provider.
*
* Cache-aware providers re-derive the split from the original (un-rendered)
* system string via `splitSystemPrompt` — `renderSystemForWire` is the
* marker-free counterpart used to build the actual wire bytes.
*
* No-op when the input has no marker. Pure / no I/O.
*/
function renderSystemForWire(system) {
	if (system.length === 0) return system;
	const parts = splitSystemPrompt(system);
	if (parts.dynamic.length === 0) return parts.static;
	if (parts.static.length === 0) return parts.dynamic;
	return `${parts.static}\n\n${parts.dynamic}`;
}
/** True when `system` contains the boundary marker. */
function hasSystemPromptBoundary(system) {
	return system.includes(SYSTEM_PROMPT_BOUNDARY);
}
/** Strip a single trailing `\n\n` (and any leftover trailing whitespace within that pair). */
function trimTrailingBlankLine(s) {
	let end = s.length;
	let newlines = 0;
	while (end > 0 && newlines < 2) {
		const ch = s.charCodeAt(end - 1);
		if (ch === 10) {
			newlines += 1;
			end -= 1;
			continue;
		}
		if (ch === 32 || ch === 9) {
			end -= 1;
			continue;
		}
		break;
	}
	return newlines > 0 ? s.slice(0, end) : s;
}
/** Strip a single leading `\n\n` (and any leftover leading whitespace within that pair). */
function trimLeadingBlankLine(s) {
	let start = 0;
	let newlines = 0;
	while (start < s.length && newlines < 2) {
		const ch = s.charCodeAt(start);
		if (ch === 10) {
			newlines += 1;
			start += 1;
			continue;
		}
		if (ch === 32 || ch === 9) {
			start += 1;
			continue;
		}
		break;
	}
	return newlines > 0 ? s.slice(start) : s;
}
//#endregion
//#region src/providers/cost.ts
/**
* Per-MTok rates for models not yet in pi-ai's bundled registry. Keyed by
* `provider:modelId`. Consulted only when pi-ai's `getModel` misses, so a
* later dep bump that bundles the model transparently takes over.
*/
const EXTRA_COST_RATES = {
	"anthropic:claude-fable-5": {
		input: 10,
		output: 50,
		cacheRead: 1,
		cacheWrite: 12.5
	},
	"anthropic:claude-opus-4-8": {
		input: 5,
		output: 25,
		cacheRead: .5,
		cacheWrite: 6.25
	},
	"arcee:trinity-large-thinking": {
		input: .25,
		output: .8
	},
	"arcee:trinity-large-preview": {
		input: .45,
		output: .15
	},
	"arcee:trinity-mini": {
		input: .045,
		output: .15
	},
	"baseten:zai-org/GLM-5": {
		input: .95,
		output: 3.15,
		cacheRead: .2
	},
	"baseten:zai-org/GLM-4.7": {
		input: .6,
		output: 2.2,
		cacheRead: .12
	},
	"baseten:moonshotai/Kimi-K2.6": {
		input: 1,
		output: 3.9,
		cacheRead: .2
	},
	"baseten:moonshotai/Kimi-K2.5": {
		input: .6,
		output: 3,
		cacheRead: .12
	},
	"baseten:deepseek-ai/DeepSeek-V4-Pro": {
		input: 1.74,
		output: 3.48,
		cacheRead: .145
	},
	"baseten:nvidia/Nemotron-120B-A12B": {
		input: .3,
		output: .75,
		cacheRead: .06
	},
	"baseten:openai/gpt-oss-120b": {
		input: .1,
		output: .5
	}
};
/**
* Fill in `usage.cost` from pi-ai's bundled price registry when the
* provider didn't report one. Provider-reported cost (e.g. OpenRouter's
* `total_cost`, OpenAI's pi-ai-computed total) always wins — we only
* estimate when `usage.cost` is undefined.
*
* Lookup is `(provider, usage.modelId)`. On a miss (unknown / unbundled
* model), `cost` stays undefined and the footer indicator hides — better
* than fabricating a $0.
*
* The number is an estimate: token counts come from the API, rates from
* the locally-bundled registry that refreshes with `@earendil-works/pi-ai`
* version bumps. If a provider changes prices between bumps the figure
* will drift until the dep updates. An optional `multiplier` scales the
* base rates for premium modes (e.g. fast mode) before totaling.
*/
function fillEstimatedCost(usage, provider, multiplier) {
	if (usage.cost !== void 0) return usage;
	if (!usage.modelId) return usage;
	const lookup = getModel;
	let model;
	try {
		model = lookup(provider, usage.modelId);
	} catch {
		model = void 0;
	}
	const rates = model?.cost ?? EXTRA_COST_RATES[`${provider}:${usage.modelId}`];
	if (!rates) return usage;
	const mul = multiplier ?? {};
	const total = ((rates.input ?? 0) * (mul.input ?? 1) * (usage.input ?? 0) + (rates.output ?? 0) * (mul.output ?? 1) * (usage.output ?? 0) + (rates.cacheRead ?? 0) * (mul.cacheRead ?? 1) * (usage.cacheRead ?? 0) + (rates.cacheWrite ?? 0) * (mul.cacheWrite ?? 1) * (usage.cacheCreation ?? 0)) / 1e6;
	if (total <= 0) return usage;
	return {
		...usage,
		cost: total
	};
}
//#endregion
//#region src/providers/schema-sanitize.ts
/** Max nesting depth before sub-schemas are replaced with `{}`. */
const MAX_DEPTH = 32;
/** Max `$ref` hop count before resolution gives up. */
const MAX_REF_HOPS = 16;
/** Keys that hold a single sub-schema. */
const SUBSCHEMA_KEYS = [
	"items",
	"additionalProperties",
	"contains",
	"not",
	"if",
	"then",
	"else",
	"propertyNames"
];
/**
* Keys that hold a record of sub-schemas.
*
* `$defs` / `definitions` are intentionally NOT included — they're
* metaschema storage, only reached via `$ref` resolution (which has its
* own walker in `resolveRef`), and `enforceRoot` strips them at the
* wire layer once inlining is done. Recursing into them would cost
* cycles and produce warnings about sub-trees the provider never sees.
*/
const SUBSCHEMA_RECORD_KEYS = [
	"properties",
	"patternProperties",
	"dependentSchemas"
];
/** Keys that hold an array of sub-schemas. */
const SUBSCHEMA_ARRAY_KEYS = [
	"oneOf",
	"anyOf",
	"allOf",
	"prefixItems"
];
function isPlainObject(value) {
	return typeof value === "object" && value !== null && !Array.isArray(value);
}
/**
* Resolve a JSON pointer like `#/$defs/Foo` against `root`. Returns
* `undefined` when the pointer doesn't resolve or hops past `MAX_REF_HOPS`.
* The caller treats `undefined` as "give up, leave as a permissive schema".
*/
function resolveRef(root, ref, hops = 0) {
	if (hops > MAX_REF_HOPS) return void 0;
	if (!ref.startsWith("#/")) return void 0;
	const parts = ref.slice(2).split("/").map(decodeRefSegment);
	let cursor = root;
	for (const part of parts) {
		if (!isPlainObject(cursor)) return void 0;
		cursor = cursor[part];
	}
	if (!isPlainObject(cursor)) return void 0;
	if (typeof cursor.$ref === "string") return resolveRef(root, cursor.$ref, hops + 1);
	return cursor;
}
function decodeRefSegment(seg) {
	return seg.replace(/~1/g, "/").replace(/~0/g, "~");
}
/**
* Recursively sanitize a sub-schema. Returns the **original** reference
* when nothing needed to change — every turn's `formatTools` re-runs the
* sanitizer, and the no-alloc fast path keeps the hot loop cheap when
* the registered tool set is already clean. Returns a fresh object when
* any rewrite happened; never mutates the input.
*
* The dirty-tracking is local to each frame so a clean sub-tree under a
* dirty parent doesn't get cloned uselessly.
*/
function sanitizeNode(node, ctx, depth, path) {
	if (depth > MAX_DEPTH) {
		ctx.warnings.push(`${ctx.prefix}schema nested deeper than ${MAX_DEPTH} levels at ${path || "$"} — replaced with permissive {}`);
		return {};
	}
	if (!isPlainObject(node)) return {};
	let dirty = false;
	let working = node;
	if (typeof working.$ref === "string") {
		const ref = working.$ref;
		const resolved = resolveRef(ctx.root, ref);
		const { $ref: _drop, ...rest } = working;
		if (resolved) {
			ctx.warnings.push(`${ctx.prefix}inlined $ref "${ref}" at ${path || "$"}`);
			working = {
				...resolved,
				...rest
			};
		} else {
			ctx.warnings.push(`${ctx.prefix}dropped unresolvable $ref "${ref}" at ${path || "$"}`);
			working = rest;
		}
		dirty = true;
	}
	if (working.nullable === true) {
		if (!dirty) {
			working = { ...working };
			dirty = true;
		}
		const t = working.type;
		if (typeof t === "string") {
			working.type = [t, "null"];
			ctx.warnings.push(`${ctx.prefix}converted nullable:true → type:[${t},null] at ${path || "$"}`);
		} else if (Array.isArray(t) && !t.includes("null")) {
			working.type = [...t, "null"];
			ctx.warnings.push(`${ctx.prefix}converted nullable:true → type:[…,null] at ${path || "$"}`);
		} else if (Array.isArray(t)) {} else ctx.warnings.push(`${ctx.prefix}stripped nullable:true at ${path || "$"} (no base type)`);
		delete working.nullable;
	}
	for (const key of SUBSCHEMA_KEYS) {
		const child = working[key];
		if (isPlainObject(child)) {
			const sanitized = sanitizeNode(child, ctx, depth + 1, `${path}/${key}`);
			if (sanitized !== child) {
				if (!dirty) {
					working = { ...working };
					dirty = true;
				}
				working[key] = sanitized;
			}
		}
	}
	for (const key of SUBSCHEMA_RECORD_KEYS) {
		const rec = working[key];
		if (isPlainObject(rec)) {
			let recDirty = false;
			let out = rec;
			for (const [k, v] of Object.entries(rec)) {
				const sanitized = sanitizeNode(v, ctx, depth + 1, `${path}/${key}/${k}`);
				if (sanitized !== v) {
					if (!recDirty) {
						out = { ...rec };
						recDirty = true;
					}
					out[k] = sanitized;
				}
			}
			if (recDirty) {
				if (!dirty) {
					working = { ...working };
					dirty = true;
				}
				working[key] = out;
			}
		}
	}
	for (const key of SUBSCHEMA_ARRAY_KEYS) {
		const arr = working[key];
		if (Array.isArray(arr)) {
			let arrDirty = false;
			let out = arr;
			for (let i = 0; i < arr.length; i++) {
				const sanitized = sanitizeNode(arr[i], ctx, depth + 1, `${path}/${key}/${i}`);
				if (sanitized !== arr[i]) {
					if (!arrDirty) {
						out = [...arr];
						arrDirty = true;
					}
					out[i] = sanitized;
				}
			}
			if (arrDirty) {
				if (!dirty) {
					working = { ...working };
					dirty = true;
				}
				working[key] = out;
			}
		}
	}
	return working;
}
/**
* Apply root-level coercions every supported provider expects:
*
*   - `type === 'object'` (Anthropic requires it; OpenAI tolerates more
*     but rejects unions at root in strict mode).
*   - `properties` is a plain object (Anthropic 400s when missing).
*   - No root-level `$ref` / `oneOf` / `anyOf` / `allOf` (Anthropic rejects;
*     OpenAI behaviour varies by endpoint).
*   - `$defs` / `definitions` stripped (recursive pass already inlined
*     refs; the metaschema storage is dead weight at the wire layer).
*
* Applied for every profile because portability matters more than the
* marginal permissiveness of any one host. Profile-specific extras
* (currently `$schema` stripping on `anthropic`) are gated below.
*/
function enforceRoot(schema, ctx) {
	let out = schema;
	let dirty = false;
	const dirtyOnce = () => {
		if (!dirty) {
			out = { ...schema };
			dirty = true;
		}
	};
	const t = out.type;
	if (Array.isArray(t)) if (t.includes("object")) {
		dirtyOnce();
		out.type = "object";
		if (t.length > 1) ctx.warnings.push(`${ctx.prefix}collapsed root type:[${t.join(",")}] to 'object'`);
	} else {
		dirtyOnce();
		ctx.warnings.push(`${ctx.prefix}root type:[${t.join(",")}] does not include 'object' — coerced to 'object'`);
		out.type = "object";
	}
	else if (typeof t === "string" && t !== "object") {
		dirtyOnce();
		ctx.warnings.push(`${ctx.prefix}coerced root type:'${t}' → 'object'`);
		out.type = "object";
	} else if (t === void 0) {
		dirtyOnce();
		out.type = "object";
	}
	if (!isPlainObject(out.properties)) {
		if ("properties" in out) ctx.warnings.push(`${ctx.prefix}replaced non-object 'properties' at root with {}`);
		dirtyOnce();
		out.properties = {};
	}
	for (const key of [
		"oneOf",
		"anyOf",
		"allOf"
	]) if (key in out) {
		dirtyOnce();
		ctx.warnings.push(`${ctx.prefix}stripped root '${key}' (providers require a single object schema)`);
		delete out[key];
	}
	if ("$ref" in out) {
		dirtyOnce();
		ctx.warnings.push(`${ctx.prefix}stripped root $ref`);
		delete out.$ref;
	}
	if ("$defs" in out) {
		dirtyOnce();
		delete out.$defs;
	}
	if ("definitions" in out) {
		dirtyOnce();
		delete out.definitions;
	}
	if (ctx.profile === "anthropic" && "$schema" in out) {
		dirtyOnce();
		delete out.$schema;
	}
	return out;
}
/**
* Sanitize a single tool's `inputSchema` for safe forwarding to the
* provider. Returns the rewritten schema + a list of warnings describing
* everything that changed.
*
* Never mutates the input. Returns the **same reference** when no rewrite
* was needed (clean-schema fast path) — `sanitizeToolSpecs` relies on
* this to keep the formatTools hot loop allocation-free across turns
* when the registered tool set is already wire-valid.
*/
function sanitizeToolSchema(input, options = {}) {
	const profile = options.profile ?? "permissive";
	const prefix = options.toolName ? `[tool:${options.toolName}] ` : "";
	if (!isPlainObject(input)) return {
		schema: {
			type: "object",
			properties: {}
		},
		warnings: []
	};
	const ctx = {
		root: input,
		warnings: [],
		profile,
		prefix
	};
	return {
		schema: enforceRoot(sanitizeNode(input, ctx, 0, ""), ctx),
		warnings: ctx.warnings
	};
}
/**
* Convenience: sanitize a batch of tools and emit a single de-duped
* `console.warn` per unique warning line. Returns the rewritten tools
* preserving original ordering and reference identity for clean schemas
* (no reallocation when nothing needed to change).
*
* The sanitiser runs every request, so log noise from a stable bad
* schema would multiply across turns; the de-dupe keeps the signal
* useful in production logs without dropping the first occurrence.
*/
function sanitizeToolSpecs(tools, options = {}) {
	const seen = /* @__PURE__ */ new Set();
	const out = [];
	for (const tool of tools) {
		const result = sanitizeToolSchema(tool.inputSchema, {
			profile: options.profile,
			toolName: tool.name
		});
		if (result.warnings.length === 0 && result.schema === tool.inputSchema) {
			out.push(tool);
			continue;
		}
		for (const line of result.warnings) {
			if (seen.has(line)) continue;
			seen.add(line);
			(options.onWarning ?? defaultWarn)(line);
		}
		out.push({
			...tool,
			inputSchema: result.schema
		});
	}
	return out;
}
function defaultWarn(line) {
	console.warn(`[zidane:schema] ${line}`);
}
//#endregion
//#region src/providers/openai-compat.ts
const TOOL_RESULTS_TAG = "__zidane_tool_results__";
const ASSISTANT_TOOL_CALLS_TAG = "__zidane_assistant_tc__";
/**
* Ceiling on how many UTF-16 code units may accumulate between two newline
* boundaries when parsing an SSE stream. A broken or malicious server that
* emits an unbounded stream of non-newline bytes would otherwise grow `buffer`
* without end and eventually OOM the host. Measured in JS string length (UTF-16
* units, 8M units ≈ 8–24 MB of UTF-8 input) rather than bytes — close enough
* for a pathological-stream tripwire without paying `Buffer.byteLength` on
* every chunk. Safely above the largest tool-call arg JSON we reasonably
* expect.
*/
const SSE_MAX_BUFFER_LENGTH = 8 * 1024 * 1024;
var OpenAICompatStreamError = class extends Error {
	constructor(message) {
		super(message);
		this.name = "OpenAICompatStreamError";
	}
};
function usageFromChunk(chunk) {
	const chunkUsage = chunk.usage;
	if (chunkUsage) {
		const cachedRead = chunkUsage.prompt_tokens_details?.cached_tokens;
		const cachedWrite = chunkUsage.prompt_tokens_details?.cache_creation_input_tokens ?? chunkUsage.prompt_tokens_details?.cache_write_tokens ?? chunkUsage.cache_creation_input_tokens;
		return {
			input: chunkUsage.prompt_tokens ?? 0,
			output: chunkUsage.completion_tokens ?? 0,
			cost: chunkUsage.total_cost ?? void 0,
			...typeof cachedRead === "number" && cachedRead > 0 ? { cacheRead: cachedRead } : {},
			...typeof cachedWrite === "number" && cachedWrite > 0 ? { cacheCreation: cachedWrite } : {}
		};
	}
	const promptEvalCount = chunk.prompt_eval_count;
	const evalCount = chunk.eval_count;
	if (typeof promptEvalCount === "number" || typeof evalCount === "number") return {
		input: typeof promptEvalCount === "number" ? promptEvalCount : 0,
		output: typeof evalCount === "number" ? evalCount : 0
	};
	return null;
}
async function consumeSSE(response, callbacks, signal) {
	const reader = response.body.getReader();
	const decoder = new TextDecoder();
	let buffer = "";
	let text = "";
	let thinking = "";
	let finishReason = "stop";
	let usage = {
		input: 0,
		output: 0
	};
	const tcMap = /* @__PURE__ */ new Map();
	const idNonce = Math.random().toString(36).slice(2, 10);
	const reasoningMap = /* @__PURE__ */ new Map();
	let sawReasoningDetails = false;
	try {
		while (true) {
			if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
			const { done, value } = await reader.read();
			if (done) break;
			buffer += decoder.decode(value, { stream: true });
			if (buffer.length > SSE_MAX_BUFFER_LENGTH) throw new OpenAICompatStreamError(`SSE buffer exceeded ${SSE_MAX_BUFFER_LENGTH} characters without a line boundary — upstream may be streaming non-SSE data.`);
			const lines = buffer.split("\n");
			buffer = lines.pop() || "";
			for (const line of lines) {
				if (!line.startsWith("data:")) continue;
				const data = line.slice(5).trim();
				if (data === "[DONE]") continue;
				let chunk;
				try {
					chunk = JSON.parse(data);
				} catch {
					continue;
				}
				const parsedUsage = usageFromChunk(chunk);
				if (parsedUsage) usage = parsedUsage;
				const choice = chunk.choices?.[0];
				if (!choice) continue;
				const fr = choice.finish_reason;
				if (fr) finishReason = fr;
				const delta = choice.delta;
				const reasoningDeltaArr = delta?.reasoning_details;
				if (reasoningDeltaArr && reasoningDeltaArr.length > 0) {
					sawReasoningDetails = true;
					for (const item of reasoningDeltaArr) {
						const idx = typeof item.index === "number" ? item.index : 0;
						const existing = reasoningMap.get(idx) ?? {};
						if (typeof item.text === "string") {
							existing.text = (existing.text ?? "") + item.text;
							thinking += item.text;
							callbacks.onThinking?.(item.text);
						}
						if (typeof item.summary === "string") {
							existing.summary = (existing.summary ?? "") + item.summary;
							thinking += item.summary;
							callbacks.onThinking?.(item.summary);
						}
						for (const key of [
							"type",
							"signature",
							"data",
							"format",
							"id"
						]) {
							const v = item[key];
							if (typeof v === "string") existing[key] = v;
						}
						reasoningMap.set(idx, existing);
					}
				}
				if (!sawReasoningDetails) {
					const thinkingDelta = delta?.reasoning_content ?? delta?.reasoning;
					if (thinkingDelta) {
						thinking += thinkingDelta;
						callbacks.onThinking?.(thinkingDelta);
					}
				}
				const contentDelta = delta?.content;
				if (contentDelta) {
					text += contentDelta;
					callbacks.onText(contentDelta);
				}
				const toolCallsDelta = delta?.tool_calls;
				if (toolCallsDelta) for (const tc of toolCallsDelta) {
					const existing = tcMap.get(tc.index);
					if (existing) {
						if (tc.id) existing.id = tc.id;
						existing.name ||= tc.function?.name ?? "";
						if (tc.function?.arguments) existing.args += tc.function.arguments;
					} else tcMap.set(tc.index, {
						id: tc.id || `call_${tc.index}_${idNonce}`,
						name: tc.function?.name || "",
						args: tc.function?.arguments || ""
					});
				}
			}
		}
	} finally {
		reader.releaseLock();
	}
	const toolCalls = [];
	for (const tc of tcMap.values()) {
		if (!tc.args) {
			toolCalls.push({
				id: tc.id,
				name: tc.name,
				input: {}
			});
			continue;
		}
		try {
			toolCalls.push({
				id: tc.id,
				name: tc.name,
				input: JSON.parse(tc.args)
			});
		} catch (err) {
			throw new OpenAICompatStreamError(`Tool call "${tc.name}" (${tc.id}) arguments were truncated or malformed: ${errorMessage(err)}`);
		}
	}
	const reasoningDetails = Array.from(reasoningMap.entries()).sort(([a], [b]) => a - b).map(([, item]) => item);
	return {
		text,
		thinking,
		toolCalls,
		finishReason,
		usage,
		reasoningDetails
	};
}
/**
* Encode a single image block as an OpenAI `image_url` multi-part entry.
*/
function toImageUrlPart(img) {
	return {
		type: "image_url",
		image_url: { url: `data:${img.mediaType};base64,${img.data}` }
	};
}
function documentMarker$1(doc) {
	return documentBlockMarker(doc, "document omitted");
}
/**
* Summarize a `tool_result` output for the companion-user-message path — text blocks
* are joined (separated by `\n`) so the tool message carries all textual context; image
* blocks are collected in a flat list for the companion user message.
*
* Used only on the fallback path; the native path walks `output` in-order to preserve
* text↔image interleaving.
*/
function summarizeToolResultOutput(output) {
	if (typeof output === "string") return {
		text: output,
		images: []
	};
	const texts = [];
	const images = [];
	for (const block of output) if (block.type === "text") texts.push(block.text);
	else if (block.type === "image") images.push({
		mediaType: block.mediaType,
		data: block.data
	});
	else if (block.type === "document") texts.push(documentMarker$1(block));
	return {
		text: texts.join("\n"),
		images
	};
}
function toOAIMessages(system, messages, options = {}) {
	const out = [{
		role: "system",
		content: renderSystemForWire(system)
	}];
	const nativeImageInTool = options.imageInToolResult === true;
	const reasoningEnabled = options.supportsReasoning === true;
	const reasoningMode = options.reasoningMode ?? "openrouter";
	const reasoningContentField = options.reasoningContentField ?? "reasoning";
	const normalizeNullContent = options.normalizeNullContent === true;
	const activeModel = options.model;
	const reasoningProducer = reasoningMode === "reasoning_content" ? "vllm" : "openrouter";
	for (const msg of messages) {
		const toolResults = [];
		const toolCalls = [];
		const textBlocks = [];
		const imageBlocks = [];
		const documentBlocks = [];
		const reasoningBlocks = [];
		for (const b of msg.content) switch (b.type) {
			case "tool_result":
				toolResults.push(b);
				break;
			case "tool_call":
				toolCalls.push(b);
				break;
			case "text":
				textBlocks.push(b);
				break;
			case "image":
				imageBlocks.push(b);
				break;
			case "document":
				documentBlocks.push(b);
				break;
			case "provider_reasoning":
				if (reasoningEnabled && b.producer === reasoningProducer && !(b.model && activeModel && b.model !== activeModel)) reasoningBlocks.push(b);
				break;
		}
		const reasoningDetails = reasoningBlocks.flatMap((b) => b.producer === "openrouter" ? b.details : []);
		const reasoningText = reasoningMode === "reasoning_content" ? reasoningBlocks.map((b) => b.producer === "vllm" ? b.reasoningText : "").filter(Boolean).join("") : "";
		if (toolResults.length > 0) {
			for (const tr of toolResults) {
				if (typeof tr.output === "string") {
					out.push({
						role: "tool",
						tool_call_id: tr.callId,
						content: tr.output
					});
					continue;
				}
				if (nativeImageInTool) {
					const parts = tr.output.map((block) => block.type === "image" ? toImageUrlPart({
						mediaType: block.mediaType,
						data: block.data
					}) : {
						type: "text",
						text: block.type === "document" ? documentMarker$1(block) : block.text
					});
					out.push({
						role: "tool",
						tool_call_id: tr.callId,
						content: parts
					});
					continue;
				}
				const { text, images } = summarizeToolResultOutput(tr.output);
				if (images.length === 0) {
					out.push({
						role: "tool",
						tool_call_id: tr.callId,
						content: text
					});
					continue;
				}
				const noun = images.length === 1 ? "image" : "images";
				const attachedMarker = `[${images.length} ${noun} attached — see next user message]`;
				const toolMarker = text.length > 0 ? `${text}\n\n${attachedMarker}` : attachedMarker;
				out.push({
					role: "tool",
					tool_call_id: tr.callId,
					content: toolMarker
				});
				out.push({
					role: "user",
					content: [...images.map(toImageUrlPart), {
						type: "text",
						text: `(${noun} returned by tool call ${tr.callId})`
					}]
				});
			}
			if (textBlocks.length === 0 && imageBlocks.length === 0 && documentBlocks.length === 0) continue;
		}
		if (toolCalls.length > 0) {
			const m = {
				role: "assistant",
				content: textBlocks.length > 0 ? textBlocks.map((b) => b.text).join("\n") : normalizeNullContent ? "" : null,
				tool_calls: toolCalls.map((tc) => ({
					id: tc.id,
					type: "function",
					function: {
						name: tc.name,
						arguments: JSON.stringify(tc.input)
					}
				}))
			};
			if (reasoningDetails.length > 0) m.reasoning_details = reasoningDetails;
			if (reasoningText.length > 0) m[reasoningContentField] = reasoningText;
			out.push(m);
			continue;
		}
		if (imageBlocks.length > 0 || documentBlocks.length > 0) {
			const parts = imageBlocks.map((img) => ({
				type: "image_url",
				image_url: { url: `data:${img.mediaType};base64,${img.data}` }
			}));
			for (const b of documentBlocks) parts.push({
				type: "text",
				text: documentMarker$1(b)
			});
			for (const b of textBlocks) parts.push({
				type: "text",
				text: b.text
			});
			const m = {
				role: msg.role,
				content: parts
			};
			if (msg.role === "assistant" && reasoningDetails.length > 0) m.reasoning_details = reasoningDetails;
			if (msg.role === "assistant" && reasoningText.length > 0) m[reasoningContentField] = reasoningText;
			out.push(m);
			continue;
		}
		let pushed;
		if (textBlocks.length === 1) pushed = {
			role: msg.role,
			content: textBlocks[0].text
		};
		else if (textBlocks.length > 1) pushed = {
			role: msg.role,
			content: textBlocks.map((b) => ({
				type: "text",
				text: b.text
			}))
		};
		else pushed = {
			role: msg.role,
			content: null
		};
		if (msg.role === "assistant" && reasoningDetails.length > 0) pushed.reasoning_details = reasoningDetails;
		if (msg.role === "assistant" && reasoningText.length > 0) pushed[reasoningContentField] = reasoningText;
		out.push(pushed);
	}
	return out;
}
const EPHEMERAL = { type: "ephemeral" };
/**
* Add `cache_control: { type: 'ephemeral' }` breakpoints to the system message's
* last text part and the last message's final content part.
*
* Mutates `messages` in place. Converts plain-string content into a single-element
* content array so the cache marker can attach — this shape is accepted verbatim by
* OpenRouter's Anthropic and Gemini routes and ignored by routes with automatic
* caching (OpenAI, DeepSeek, Grok, Groq, Moonshot).
*
* Skip conditions (safe no-ops):
* - Empty messages array.
* - Assistant messages with no text (tool-call-only) — attaching a cache marker to a
*   `tool_calls` block has no defined semantics, so we skip and let the prior
*   system/tools breakpoints carry caching.
*
* System-prompt boundary handling: when `originalSystem` (the un-rendered
* system text passed to {@link toOAIMessages}) contains
* {@link SYSTEM_PROMPT_BOUNDARY}, the leading system message is rewritten as
* a two-part array — `cache_control` lands on the static prefix only, so
* per-turn churn in the dynamic suffix doesn't invalidate the cached
* doctrine above. `originalSystem` defaults to `undefined` for back-compat;
* omit it (or pass a marker-free string) for the single-block historic
* behavior.
*/
function applyOAICacheBreakpoints(messages, originalSystem) {
	if (messages.length === 0) return;
	const first = messages[0];
	if (first.role === "system") markSystemMessage(first, originalSystem);
	const lastIdx = messages.length - 1;
	if (lastIdx > 0) markLastContentPart(messages[lastIdx]);
}
/**
* Place the cache marker on the static prefix of the system message. When
* `originalSystem` carried {@link SYSTEM_PROMPT_BOUNDARY}, the message becomes
* a two-part array — static (cached) then dynamic (uncached). Otherwise falls
* through to {@link markLastContentPart} for the historic single-block
* treatment.
*
* String-only branch — system messages produced by {@link toOAIMessages} are
* always plain strings (`{ role: 'system', content: <rendered> }`). Hosts
* that supply pre-built multi-part system messages bypass this branch and
* get the generic last-block treatment.
*/
function markSystemMessage(msg, originalSystem) {
	if (typeof msg.content !== "string" || msg.content.length === 0) {
		markLastContentPart(msg);
		return;
	}
	const parts = splitSystemPrompt(originalSystem && originalSystem.length > 0 ? originalSystem : msg.content);
	if (parts.dynamic.length === 0) {
		msg.content = [{
			type: "text",
			text: parts.static,
			cache_control: EPHEMERAL
		}];
		return;
	}
	const next = [];
	if (parts.static.length > 0) next.push({
		type: "text",
		text: parts.static,
		cache_control: EPHEMERAL
	});
	next.push({
		type: "text",
		text: parts.dynamic
	});
	msg.content = next;
}
/**
* Mark the last content part of an OAI message with `cache_control`. Normalizes
* string content into a `[{ type: 'text', text, cache_control }]` array so the
* marker has a block to attach to.
*
* No-op for messages without string or array content (tool-call-only assistant
* messages fall through; the system/tools breakpoints carry the cache prefix).
*/
function markLastContentPart(msg) {
	if (typeof msg.content === "string") {
		if (msg.content.length === 0) return;
		msg.content = [{
			type: "text",
			text: msg.content,
			cache_control: EPHEMERAL
		}];
		return;
	}
	if (!Array.isArray(msg.content) || msg.content.length === 0) return;
	const parts = msg.content;
	const lastBlockIdx = parts.length - 1;
	parts[lastBlockIdx] = {
		...parts[lastBlockIdx],
		cache_control: EPHEMERAL
	};
}
/**
* Return a copy of `tools` with `cache_control` on the last entry.
*
* OpenRouter accepts the marker alongside the standard `type` + `function` fields
* and forwards it when routing to Anthropic/Gemini. Leaves the non-cached tools
* unchanged so the caller's reference is not mutated.
*/
function applyOAIToolCacheBreakpoint(tools) {
	if (tools.length === 0) return tools;
	const lastIdx = tools.length - 1;
	return tools.map((tool, i) => i === lastIdx ? {
		...tool,
		cache_control: EPHEMERAL
	} : tool);
}
function formatTools(tools) {
	return sanitizeToolSpecs(tools, { profile: "openai" }).map((t) => ({
		type: "function",
		function: {
			name: t.name,
			description: t.description,
			parameters: t.inputSchema
		}
	}));
}
function userMessage(content) {
	return {
		role: "user",
		content: [{
			type: "text",
			text: content
		}]
	};
}
function assistantMessage(content) {
	return {
		role: "assistant",
		content: [{
			type: "text",
			text: content
		}]
	};
}
function toolResultsMessage(results) {
	return {
		role: "user",
		content: results.map((r) => ({
			type: "tool_result",
			callId: r.id,
			output: r.content,
			...r.isError ? { isError: true } : {}
		}))
	};
}
/**
* Recover human-readable reasoning text from an OpenRouter-style
* `reasoning_details` array. Used as a fallback when a `reasoning_content`-mode
* host emits the structured envelope instead of the plain text field — the
* `text` / `summary` fields carry the visible chain-of-thought; opaque fields
* (signature/data/format) are ignored.
*/
function extractReasoningDetailsText(details) {
	let out = "";
	for (const item of details) {
		if (!item || typeof item !== "object") continue;
		const rec = item;
		if (typeof rec.text === "string") out += rec.text;
		else if (typeof rec.summary === "string") out += rec.summary;
	}
	return out;
}
function buildAssistantContent(text, toolCalls, thinking, reasoning) {
	const content = [];
	const hasReasoning = reasoning && (reasoning.producer === "openrouter" ? reasoning.details.length > 0 : reasoning.reasoningText.length > 0);
	if (reasoning && hasReasoning) {
		const block = reasoning.producer === "openrouter" ? {
			type: "provider_reasoning",
			producer: "openrouter",
			details: reasoning.details
		} : {
			type: "provider_reasoning",
			producer: "vllm",
			reasoningText: reasoning.reasoningText
		};
		if (reasoning.model) block.model = reasoning.model;
		content.push(block);
	}
	if (thinking) content.push({
		type: "thinking",
		text: thinking
	});
	if (text) content.push({
		type: "text",
		text
	});
	for (const tc of toolCalls) content.push({
		type: "tool_call",
		id: tc.id,
		name: tc.name,
		input: tc.input
	});
	if (content.length === 0) content.push({
		type: "text",
		text: "[no content]"
	});
	return {
		role: "assistant",
		content
	};
}
/**
* HTTP error thrown when an OpenAI-compatible endpoint returns a non-OK response.
*
* The body is best-effort JSON-parsed; `error.message` / `error.code` / `error.type`
* are extracted for clean downstream classification.
*/
var OpenAICompatHttpError = class extends Error {
	status;
	providerCode;
	bodyText;
	constructor(status, bodyText) {
		let message = bodyText;
		let code;
		try {
			const parsed = JSON.parse(bodyText);
			message = parsed?.error?.message ?? bodyText;
			code = parsed?.error?.code ?? parsed?.error?.type;
		} catch {}
		super(`HTTP ${status}: ${message}`);
		this.name = "OpenAICompatHttpError";
		this.status = status;
		this.providerCode = code;
		this.bodyText = bodyText;
	}
};
const TRAILING_SLASH_RE = /\/$/;
/**
* Classify an OpenAI-compatible error into `ClassifiedError`.
*
* Recognizes:
* - `AbortError` (from fetch) → `aborted`.
* - `OpenAICompatHttpError` with a context-exceeded code or message → `context_exceeded`.
* - Any other `OpenAICompatHttpError` → `provider_error`.
*
* Returns `null` for unrecognized error shapes (the loop falls back to `AgentProviderError`).
*/
function classifyOpenAICompatError(err) {
	const prelude = classifyErrorPrelude(err);
	if (prelude === "not-object") return null;
	if (prelude === "aborted") return { kind: "aborted" };
	if (err instanceof OpenAICompatStreamError) return {
		kind: "provider_error",
		providerCode: "stream_error",
		message: err.message,
		retryable: true
	};
	if (!(err instanceof OpenAICompatHttpError)) return null;
	const code = err.providerCode;
	const msg = err.message;
	if (code === "context_length_exceeded" || matchesContextExceeded(msg)) return {
		kind: "context_exceeded",
		providerCode: code ?? "context_length_exceeded",
		message: msg
	};
	if (matchesToolPairingError(msg)) return {
		kind: "tool_pairing_corruption",
		providerCode: code ?? "invalid_request_error",
		message: msg
	};
	return {
		kind: "provider_error",
		providerCode: code ?? String(err.status),
		message: msg,
		retryable: isRetryableHttpStatus(err.status)
	};
}
/**
* Map an OpenAI-compatible `finish_reason` string to the zidane `TurnFinishReason` union.
*/
function mapOAIFinishReason(reason) {
	if (!reason) return void 0;
	switch (reason) {
		case "stop": return "stop";
		case "tool_calls":
		case "function_call": return "tool-calls";
		case "length": return "length";
		case "content_filter": return "content-filter";
		default: return "other";
	}
}
/**
* Map zidane's `ThinkingLevel` + optional explicit budget to the OpenRouter
* `reasoning` request field. Returns `undefined` when reasoning should not be
* sent (off, or no level + no budget).
*
* - `'low' | 'medium' | 'high'` → `{ effort }`.
* - `'minimal'` → `{ effort: 'low' }` (closest match in OpenRouter's vocabulary).
* - `'adaptive'` → `{}` (let upstream decide; OpenRouter passes through).
* - explicit `thinkingBudget` → `{ max_tokens }` overlaid on the level.
*/
function planOpenRouterReasoning(thinking, thinkingBudget) {
	if ((!thinking || thinking === "off") && typeof thinkingBudget !== "number") return void 0;
	const out = {};
	if (thinking && thinking !== "off" && thinking !== "adaptive") out.effort = thinking === "minimal" ? "low" : thinking;
	if (typeof thinkingBudget === "number" && thinkingBudget > 0) out.max_tokens = thinkingBudget;
	return out;
}
function usageWasReported(usage) {
	return usage.input > 0 || usage.output > 0 || (usage.cacheRead ?? 0) > 0 || (usage.cacheCreation ?? 0) > 0 || usage.cost !== void 0;
}
function estimateMissingUsage(body, result) {
	return {
		input: estimateTokens(JSON.stringify({
			messages: body.messages,
			tools: body.tools
		})),
		output: estimateTokens([
			result.thinking,
			result.text,
			result.toolCalls.length > 0 ? JSON.stringify(result.toolCalls) : ""
		].filter(Boolean).join("\n"))
	};
}
/**
* Factory for any OpenAI-compatible HTTP endpoint.
*
* Speaks the standard `POST /chat/completions` + `stream: true` + SSE dialect.
* Thin wrappers (`openrouter`, `cerebras`) call this with pinned defaults.
*
* @example Baseten (non-standard auth scheme)
* ```ts
* openaiCompat({
*   name: 'baseten',
*   apiKey: process.env.BASETEN_API_KEY!,
*   baseURL: process.env.BASETEN_PROXY_URL!,
*   authHeader: { name: 'Authorization', scheme: 'Api-Key' },
* })
* ```
*/
function openaiCompat(params) {
	const name = params.name ?? "openai-compat";
	const defaultModel = params.defaultModel ?? "gpt-4o-mini";
	const authHeaderName = params.authHeader?.name ?? "Authorization";
	const authHeaderValue = params.authHeader?.scheme ? `${params.authHeader.scheme} ${params.apiKey}` : params.authHeader ? params.apiKey : `Bearer ${params.apiKey}`;
	const extraHeaders = params.extraHeaders ?? {};
	const endpoint = `${params.baseURL.replace(TRAILING_SLASH_RE, "")}/chat/completions`;
	const capabilities = {
		vision: params.capabilities?.vision ?? false,
		imageInToolResult: params.capabilities?.imageInToolResult ?? false
	};
	const cacheBreakpointsEnabled = params.cacheBreakpoints === true;
	const reasoningEnabled = params.supportsReasoning === true;
	const reasoningMode = params.reasoningMode ?? "openrouter";
	const reasoningContentField = params.reasoningContentField ?? "reasoning";
	const normalizeNullContent = params.normalizeNullContent === true;
	return {
		name,
		meta: {
			defaultModel,
			capabilities
		},
		formatTools,
		userMessage,
		assistantMessage,
		toolResultsMessage,
		classifyError: classifyOpenAICompatError,
		async stream(options, callbacks) {
			const modelId = options.model || defaultModel;
			const messages = toOAIMessages(options.system, options.messages, {
				imageInToolResult: capabilities.imageInToolResult === true,
				supportsReasoning: reasoningEnabled,
				reasoningMode,
				reasoningContentField,
				normalizeNullContent,
				model: modelId
			});
			const shouldCache = cacheBreakpointsEnabled && options.cache !== false;
			if (shouldCache) applyOAICacheBreakpoints(messages, options.system);
			const maxTokens = options.thinkingBudget ? options.thinkingBudget + options.maxTokens : options.maxTokens;
			const reasoningBody = params.planReasoningRequest?.({
				model: modelId,
				thinking: options.thinking,
				thinkingBudget: options.thinkingBudget
			});
			const body = {
				...params.extraBodyParams ?? {},
				...reasoningBody ?? {},
				model: modelId,
				messages,
				max_tokens: maxTokens,
				stream: true
			};
			if (reasoningEnabled && reasoningMode === "openrouter") {
				const reasoning = planOpenRouterReasoning(options.thinking, options.thinkingBudget);
				if (reasoning) body.reasoning = reasoning;
			}
			if (options.tools && options.tools.length > 0) body.tools = shouldCache ? applyOAIToolCacheBreakpoint(options.tools) : options.tools;
			if (options.toolChoice) if (options.toolChoice.type === "tool" && options.toolChoice.name) body.tool_choice = {
				type: "function",
				function: { name: options.toolChoice.name }
			};
			else if (options.toolChoice.type === "required") body.tool_choice = "required";
			else body.tool_choice = "auto";
			const response = await fetch(endpoint, {
				method: "POST",
				headers: {
					[authHeaderName]: authHeaderValue,
					"Content-Type": "application/json",
					...extraHeaders
				},
				body: JSON.stringify(body),
				signal: options.signal
			});
			if (!response.ok) {
				const errorText = await response.text();
				throw new OpenAICompatHttpError(response.status, errorText);
			}
			const result = await consumeSSE(response, callbacks, options.signal);
			const finishReason = mapOAIFinishReason(result.finishReason);
			const usage = usageWasReported(result.usage) ? result.usage : estimateMissingUsage(body, result);
			let reasoningArg;
			let thinkingForBlock = result.thinking;
			if (reasoningEnabled) {
				if (reasoningMode === "reasoning_content") {
					const reasoningText = result.thinking.length > 0 ? result.thinking : extractReasoningDetailsText(result.reasoningDetails);
					if (reasoningText.length > 0) {
						reasoningArg = {
							reasoningText,
							producer: "vllm",
							model: modelId
						};
						thinkingForBlock = void 0;
					}
				} else if (result.reasoningDetails.length > 0) reasoningArg = {
					details: result.reasoningDetails,
					producer: "openrouter",
					model: modelId
				};
			}
			return {
				assistantMessage: buildAssistantContent(result.text, result.toolCalls, thinkingForBlock, reasoningArg),
				text: result.text,
				toolCalls: result.toolCalls,
				done: result.toolCalls.length === 0,
				usage: fillEstimatedCost({
					input: usage.input,
					output: usage.output,
					...usage.cacheRead !== void 0 ? { cacheRead: usage.cacheRead } : {},
					...usage.cacheCreation !== void 0 ? { cacheCreation: usage.cacheCreation } : {},
					...usage.cost !== void 0 ? { cost: usage.cost } : {},
					...finishReason ? { finishReason } : {},
					modelId
				}, name)
			};
		}
	};
}
//#endregion
//#region src/session/messages.ts
/**
* Decode an Anthropic `tool_result.content` wire value back to zidane's canonical
* `string | ToolResultContent[]` shape.
*
* Anthropic accepts three shapes in the wire: `string`, `Array<{type:'text',text}>`,
* or `Array<{type:'text'|'image', ...}>`. We collapse to `string` only when every
* block is text to keep simple cases readable.
*/
function decodeAnthropicToolResultContent(content) {
	if (typeof content === "string") return content;
	if (!Array.isArray(content)) return JSON.stringify(content);
	const blocks = [];
	for (const raw of content) {
		if (!raw || typeof raw !== "object") continue;
		const b = raw;
		if (b.type === "text" && typeof b.text === "string") {
			blocks.push({
				type: "text",
				text: b.text
			});
			continue;
		}
		if (b.type === "image" && b.source && typeof b.source === "object") {
			const src = b.source;
			if (src.type === "base64" && typeof src.data === "string" && typeof src.media_type === "string") {
				blocks.push({
					type: "image",
					mediaType: src.media_type,
					data: src.data
				});
				continue;
			}
		}
		blocks.push({
			type: "text",
			text: JSON.stringify(raw)
		});
	}
	if (blocks.length === 0) return "";
	if (!blocks.some((b) => b.type !== "text")) return blocks.map((b) => b.text).join("\n");
	return blocks;
}
/**
* Build an Anthropic `image.source` block, reconciling the declared media
* type against the actual magic bytes.
*
* Anthropic's API rejects the entire request when `media_type` disagrees
* with the payload (`messages.N.content.K.image.source.base64: The image
* was specified using the X media type, but the image appears to be a
* Y image`). The upstream sources of `{type:'image'}` blocks each sniff
* before producing the block (read_file / MCP / TUI paste), but this
* wire-boundary reconciliation is a final safety net for any future code
* path or host-supplied prompt part that slips through without sniffing.
*
* The reconciler is conservative: it overrides only when the magic bytes
* positively identify a *different* known format. Unknown / too-short
* payloads pass `mediaType` through unchanged.
*/
function anthropicImageSource(mediaType, data) {
	return {
		type: "base64",
		media_type: reconcileImageMediaType(mediaType, data),
		data
	};
}
function anthropicDocumentBlocks(doc) {
	if (doc.encoding === "base64" && doc.mediaType === "application/pdf") return [{
		type: "document",
		source: {
			type: "base64",
			media_type: "application/pdf",
			data: doc.data
		},
		...doc.name ? { title: doc.name } : {}
	}];
	return [{
		type: "text",
		text: `${doc.name ? `<attachment name="${doc.name}" media_type="${doc.mediaType}" encoding="${doc.encoding}">` : `<attachment media_type="${doc.mediaType}" encoding="${doc.encoding}">`}\n${doc.data}\n</attachment>`
	}];
}
function documentMarker(doc) {
	return documentBlockMarker(doc, "document attached");
}
/**
* Coerce a tool-call id to Anthropic's wire constraint `^[a-zA-Z0-9_-]+$`.
*
* Sessions started under another provider (OpenAI-compat / OpenRouter /
* Cerebras) or carrying MCP-namespaced calls can hold ids with `.`, `:`,
* `/` or other characters that 400 the request the moment the user switches
* to an Anthropic model (`messages.N.content.M.tool_use.id: String should
* match pattern '^[a-zA-Z0-9_-]+$'`).
*
* The mapping is deterministic — every disallowed char becomes `_` — so a
* `tool_use` and its paired `tool_result` (sanitized identically below) keep
* matching ids and the pairing survives. When sanitization actually changed
* the id, a short hash of the ORIGINAL id is appended so distinct inputs
* that would collapse to the same replacement (`call.1` vs `call_1` vs
* `call:1`) stay distinct on the wire. Empty ids get a stable placeholder
* so the block stays well-formed.
*/
const ANTHROPIC_TOOL_ID_INVALID = /[^\w-]/g;
function sanitizeAnthropicToolId(id) {
	if (!id) return "tool_0";
	const sanitized = id.replace(ANTHROPIC_TOOL_ID_INVALID, "_");
	if (sanitized === id) return id;
	return `${sanitized}_${shortIdHash(id)}`;
}
/**
* Tiny FNV-1a hash rendered in base36 — collision-resistant enough to keep
* sanitized tool ids distinct, deterministic so `tool_use` / `tool_result`
* pairs derived from the same original id always match.
*/
function shortIdHash(value) {
	let hash = 2166136261;
	for (let i = 0; i < value.length; i++) {
		hash ^= value.charCodeAt(i);
		hash = Math.imul(hash, 16777619);
	}
	return (hash >>> 0).toString(36);
}
/**
* Encode zidane's canonical `string | ToolResultContent[]` shape to Anthropic's
* wire format for `tool_result.content`.
*/
function encodeAnthropicToolResultContent(output) {
	if (typeof output === "string") return output;
	return output.map((b) => {
		if (b.type === "text") return {
			type: "text",
			text: b.text
		};
		if (b.type === "document") return {
			type: "text",
			text: documentMarker(b)
		};
		return {
			type: "image",
			source: anthropicImageSource(b.mediaType, b.data)
		};
	});
}
function fromAnthropic(msg) {
	const role = msg.role;
	const content = [];
	if (typeof msg.content === "string") {
		content.push({
			type: "text",
			text: msg.content
		});
		return {
			role,
			content
		};
	}
	if (Array.isArray(msg.content)) for (const block of msg.content) {
		if (!block || typeof block !== "object") continue;
		const b = block;
		if (b.type === "text") content.push({
			type: "text",
			text: b.text
		});
		else if (b.type === "image") {
			const source = b.source;
			if (source?.type === "base64") content.push({
				type: "image",
				mediaType: source.media_type,
				data: source.data
			});
		} else if (b.type === "document") {
			const source = b.source;
			if (source?.type === "base64" && typeof source.media_type === "string" && typeof source.data === "string") content.push({
				type: "document",
				mediaType: source.media_type,
				data: source.data,
				encoding: "base64",
				...typeof b.title === "string" ? { name: b.title } : {}
			});
			else if (source?.type === "text" && typeof source.media_type === "string" && typeof source.data === "string") content.push({
				type: "document",
				mediaType: source.media_type,
				data: source.data,
				encoding: "text",
				...typeof b.title === "string" ? { name: b.title } : {}
			});
		} else if (b.type === "tool_use") content.push({
			type: "tool_call",
			id: b.id,
			name: b.name,
			input: b.input
		});
		else if (b.type === "tool_result") {
			const output = decodeAnthropicToolResultContent(b.content);
			const block = {
				type: "tool_result",
				callId: b.tool_use_id,
				output
			};
			if (b.is_error === true) block.isError = true;
			content.push(block);
		} else if (b.type === "thinking") {
			const block = {
				type: "thinking",
				text: b.thinking ?? ""
			};
			if (typeof b.signature === "string") {
				block.signature = b.signature;
				block.signatureProducer = "anthropic";
			}
			content.push(block);
		} else if (b.type === "redacted_thinking") content.push({
			type: "redacted_thinking",
			data: b.data ?? ""
		});
		else if (b.type === "server_tool_use") content.push({
			type: "server_tool_use",
			producer: "anthropic",
			id: b.id ?? "",
			name: b.name ?? "",
			input: b.input ?? {}
		});
		else if (b.type === "web_search_tool_result") content.push({
			type: "server_tool_result",
			producer: "anthropic",
			toolUseId: b.tool_use_id ?? "",
			toolName: "web_search",
			content: b.content
		});
	}
	return {
		role,
		content
	};
}
function fromOpenAI(msg) {
	const role = msg.role;
	const content = [];
	const c = msg.content;
	if (c == null) return {
		role,
		content
	};
	if (typeof c === "string") {
		content.push({
			type: "text",
			text: c
		});
		return {
			role,
			content
		};
	}
	if (typeof c === "object" && !Array.isArray(c) && c._tag === "__zidane_assistant_tc__") {
		const tagged = c;
		if (typeof tagged.text === "string" && tagged.text) content.push({
			type: "text",
			text: tagged.text
		});
		if (Array.isArray(tagged.tool_calls)) for (const raw of tagged.tool_calls) {
			if (!raw || typeof raw !== "object") continue;
			const tc = raw;
			const rawArgs = tc.function?.arguments;
			let input = {};
			if (typeof rawArgs === "string") try {
				input = JSON.parse(rawArgs);
			} catch {
				input = {};
			}
			else if (rawArgs && typeof rawArgs === "object") input = rawArgs;
			content.push({
				type: "tool_call",
				id: tc.id ?? "",
				name: tc.function?.name ?? "",
				input
			});
		}
		return {
			role,
			content
		};
	}
	if (typeof c === "object" && !Array.isArray(c) && c._tag === "__zidane_tool_results__") {
		const tagged = c;
		if (Array.isArray(tagged.results)) for (const raw of tagged.results) {
			if (!raw || typeof raw !== "object") continue;
			const r = raw;
			content.push({
				type: "tool_result",
				callId: r.tool_call_id ?? "",
				output: r.content ?? ""
			});
		}
		if (typeof tagged.text === "string" && tagged.text) content.push({
			type: "text",
			text: tagged.text
		});
		return {
			role,
			content
		};
	}
	if (Array.isArray(c)) {
		for (const block of c) {
			if (!block || typeof block !== "object") continue;
			const b = block;
			if (b.type === "text") content.push({
				type: "text",
				text: b.text
			});
			else if (b.type === "image_url") {
				const imageUrl = b.image_url?.url;
				if (imageUrl?.startsWith("data:")) {
					const [meta, data] = imageUrl.slice(5).split(",", 2);
					const mediaType = meta.replace(";base64", "");
					content.push({
						type: "image",
						mediaType,
						data
					});
				}
			}
		}
		return {
			role,
			content
		};
	}
	return {
		role,
		content
	};
}
function toAnthropic(msg) {
	const blocks = msg.content.filter((b) => {
		if (b.type === "provider_reasoning") return false;
		if (b.type !== "thinking") return true;
		if (b.signatureProducer === "openai") return false;
		if (!b.signature) return false;
		return true;
	}).flatMap((block) => {
		switch (block.type) {
			case "text": return [{
				type: "text",
				text: block.text
			}];
			case "image": return [{
				type: "image",
				source: anthropicImageSource(block.mediaType, block.data)
			}];
			case "document": return anthropicDocumentBlocks(block);
			case "tool_call": return [{
				type: "tool_use",
				id: sanitizeAnthropicToolId(block.id),
				name: block.name,
				input: block.input
			}];
			case "tool_result": {
				const out = {
					type: "tool_result",
					tool_use_id: sanitizeAnthropicToolId(block.callId),
					content: encodeAnthropicToolResultContent(block.output)
				};
				if (block.isError) out.is_error = true;
				return [out, ...(typeof block.output === "string" ? [] : block.output.filter((part) => part.type === "document")).flatMap(anthropicDocumentBlocks)];
			}
			case "thinking": return [{
				type: "thinking",
				thinking: block.text,
				signature: block.signature ?? ""
			}];
			case "redacted_thinking": return [{
				type: "redacted_thinking",
				data: block.data
			}];
			case "server_tool_use": return [{
				type: "server_tool_use",
				id: sanitizeAnthropicToolId(block.id),
				name: block.name,
				input: block.input
			}];
			case "server_tool_result": return [{
				type: "web_search_tool_result",
				tool_use_id: sanitizeAnthropicToolId(block.toolUseId),
				content: block.content
			}];
			default: return [{
				type: "text",
				text: ""
			}];
		}
	});
	const first = blocks[0];
	if (blocks.length === 1 && first.type === "text") return {
		role: msg.role,
		content: first.text
	};
	return {
		role: msg.role,
		content: blocks
	};
}
function toOpenAI(msg) {
	const toolCalls = msg.content.filter((b) => b.type === "tool_call");
	const toolResults = msg.content.filter((b) => b.type === "tool_result");
	const textBlocks = msg.content.filter((b) => b.type === "text");
	const imageBlocks = msg.content.filter((b) => b.type === "image");
	const documentBlocks = msg.content.filter((b) => b.type === "document");
	if (toolResults.length > 0) {
		const siblingText = textBlocks.map((b) => b.text).join("\n");
		return {
			role: msg.role,
			content: {
				_tag: TOOL_RESULTS_TAG,
				results: toolResults.map((tr) => ({
					tool_call_id: tr.callId,
					content: tr.output
				})),
				...siblingText ? { text: siblingText } : {}
			}
		};
	}
	if (toolCalls.length > 0) {
		const textContent = textBlocks.length > 0 ? textBlocks[0].text : null;
		return {
			role: msg.role,
			content: {
				_tag: ASSISTANT_TOOL_CALLS_TAG,
				text: textContent,
				tool_calls: toolCalls.map((tc) => ({
					id: tc.id,
					type: "function",
					function: {
						name: tc.name,
						arguments: JSON.stringify(tc.input)
					}
				}))
			}
		};
	}
	if (imageBlocks.length > 0 || documentBlocks.length > 0) {
		const parts = imageBlocks.map((img) => ({
			type: "image_url",
			image_url: { url: `data:${img.mediaType};base64,${img.data}` }
		}));
		for (const b of documentBlocks) parts.push({
			type: "text",
			text: documentMarker(b)
		});
		for (const b of textBlocks) parts.push({
			type: "text",
			text: b.text
		});
		return {
			role: msg.role,
			content: parts
		};
	}
	if (textBlocks.length === 1) return {
		role: msg.role,
		content: textBlocks[0].text
	};
	if (textBlocks.length > 1) return {
		role: msg.role,
		content: textBlocks.map((b) => ({
			type: "text",
			text: b.text
		}))
	};
	return {
		role: msg.role,
		content: null
	};
}
/**
* Placeholder content inserted into a synthetic `tool_result` when the harness
* has to repair an orphan `tool_use`. Exported so downstream consumers
* (training-data collectors, HFI submission) can reject any payload containing
* it — the marker satisfies the wire-level pairing contract structurally but
* the content itself is fake and would poison fine-tuning data.
*/
const SYNTHETIC_TOOL_RESULT_PLACEHOLDER = "[Tool result missing due to internal error]";
/**
* Replacement text for an assistant message whose every content block was
* stripped during pairing repair (e.g. the only blocks were orphan
* `tool_call`s). Providers reject empty `content` arrays — the marker keeps
* the turn shape valid while signaling "this assistant turn lost its
* outputs" to the model so it can recover.
*/
const TOOL_USE_INTERRUPTED_MARKER = "[Tool use interrupted]";
/**
* Replacement text for a user message whose every content block was a
* `tool_result` with no matching upstream `tool_call` (e.g. the assistant
* pair was stripped by an earlier compaction). Same role as
* {@link TOOL_USE_INTERRUPTED_MARKER} on the user side.
*/
const ORPHANED_TOOL_RESULT_MARKER = "[Orphaned tool result removed due to conversation resume]";
/**
* Defensive repair pass that rewrites a message list so it satisfies the
* wire-level `tool_use` ↔ `tool_result` adjacency contract every modern
* provider enforces.
*
* Anthropic 400s on orphans with `'tool_use' ids were found without
* 'tool_result' blocks immediately after` (and its inverse, `tool_result
* must be preceded by a tool_call with the same toolCallId`). OpenAI's
* Chat Completions API rejects most mismatches with a 400 too.
*
* Six repair modes — modeled after Anthropic's Claude Code defenses:
*
* | # | Corruption | Repair |
* |---|------------|--------|
* | 1 | assistant `tool_use` whose next user msg lacks a matching `tool_result` | **Prepend** a synthetic `tool_result` block carrying {@link SYNTHETIC_TOOL_RESULT_PLACEHOLDER} with `isError: true` |
* | 2 | assistant `tool_use` followed by nothing (or by a non-user msg) | **Insert** a new synthetic user message with the same placeholder |
* | 3 | user `tool_result` with no preceding assistant `tool_call` | **Strip** the orphan block; if it empties the msg, replace with {@link ORPHANED_TOOL_RESULT_MARKER} text block |
* | 4 | duplicate `tool_call.id` across assistant messages (CC-1212) | **Strip** later instances + their matching tool_results |
* | 5 | duplicate `tool_result.callId` within a single user message | **Dedupe** by `callId`, keep first |
* | 6 | assistant message emptied by mode-4 stripping | **Replace** content with {@link TOOL_USE_INTERRUPTED_MARKER} text block |
*
* **Repair, not drop.** Earlier versions of this pass simply dropped orphan
* blocks, which (a) silently rewrote history the model had reasoned over and
* (b) cascaded — dropping an assistant tool_call would orphan its
* tool_result, which would then get dropped too, removing any trace of
* "what the model tried to do" from the transcript. The repair-based pass
* preserves the model's tool_use shape and patches the dangling result with
* an `is_error` placeholder so the model sees "I tried X, the result was
* lost" and can retry intelligently.
*
* Adjacency contract: `tool_result` blocks must live in the user message
* IMMEDIATELY following the assistant message that emitted the matching
* `tool_use`. The pass enforces strict adjacency — a tool_result two
* messages downstream of its tool_call is still an orphan.
*
* Idempotent: returns the input reference unchanged when no repairs were
* necessary. Re-running on already-repaired output is a no-op.
*
* Pure: does not mutate input messages or their content arrays — every
* repair allocates a fresh array / object.
*/
function ensureToolResultPairing(messages, options = {}) {
	if (messages.length === 0) return messages;
	const fireRepair = options.onRepair ?? (() => {});
	const firstSeenAt = /* @__PURE__ */ new Map();
	for (let i = 0; i < messages.length; i++) {
		const msg = messages[i];
		if (msg.role !== "assistant") continue;
		for (const block of msg.content) if (block.type === "tool_call" && !firstSeenAt.has(block.id)) firstSeenAt.set(block.id, i);
	}
	const pendingOrphansByUserIndex = /* @__PURE__ */ new Map();
	let changed = false;
	const markChanged = () => {
		changed = true;
	};
	const out = [];
	for (let i = 0; i < messages.length; i++) {
		const msg = messages[i];
		if (msg.role === "assistant") processAssistantMessage({
			index: i,
			msg,
			messages,
			firstSeenAt,
			pendingOrphansByUserIndex,
			out,
			fireRepair,
			markChanged
		});
		else processUserMessage({
			index: i,
			msg,
			pendingOrphansByUserIndex,
			out,
			fireRepair,
			markChanged
		});
	}
	return changed ? out : messages;
}
/**
* Process one assistant message: strip mode-4 duplicates, queue mode-1 / 2
* orphan-tool-use repairs for the next user message (or insert a synthetic
* one), and emit a mode-6 marker when dedup empties the message.
*/
function processAssistantMessage(args) {
	const { index, msg, messages, firstSeenAt, pendingOrphansByUserIndex, out, fireRepair, markChanged } = args;
	const dedupedContent = [];
	const seenHere = /* @__PURE__ */ new Set();
	let dedupedChanged = false;
	for (const block of msg.content) {
		if (block.type === "tool_call") {
			const isCrossMsgDup = firstSeenAt.get(block.id) !== index;
			const isIntraMsgDup = seenHere.has(block.id);
			if (isCrossMsgDup || isIntraMsgDup) {
				dedupedChanged = true;
				fireRepair({
					mode: "duplicate-tool-use-strip",
					callId: block.id,
					messageIndex: index
				});
				continue;
			}
			seenHere.add(block.id);
		}
		dedupedContent.push(block);
	}
	const surviving = collectIds(dedupedContent, "tool_call", (b) => b.id);
	const next = messages[index + 1];
	const nextIsUser = next && next.role === "user";
	const nextResultIds = nextIsUser ? collectResultIds(next.content) : /* @__PURE__ */ new Set();
	const orphans = [];
	for (const id of surviving) if (!nextResultIds.has(id)) orphans.push(id);
	emitAssistantAfterDedup({
		index,
		msg,
		dedupedContent,
		dedupedChanged,
		out,
		fireRepair,
		markChanged
	});
	if (orphans.length === 0) return;
	if (nextIsUser) {
		markChanged();
		pendingOrphansByUserIndex.set(index + 1, orphans);
		for (const callId of orphans) fireRepair({
			mode: "orphan-tool-use-prepend",
			callId,
			messageIndex: index
		});
		return;
	}
	markChanged();
	out.push({
		role: "user",
		content: orphans.map((callId) => syntheticResultBlock(callId))
	});
	for (const callId of orphans) fireRepair({
		mode: "orphan-tool-use-append",
		callId,
		messageIndex: index
	});
}
function emitAssistantAfterDedup(args) {
	const { index, msg, dedupedContent, dedupedChanged, out, fireRepair, markChanged } = args;
	if (!dedupedChanged) {
		out.push(msg);
		return;
	}
	markChanged();
	if (dedupedContent.length === 0) {
		out.push({
			...msg,
			content: [{
				type: "text",
				text: TOOL_USE_INTERRUPTED_MARKER
			}]
		});
		fireRepair({
			mode: "empty-assistant-marker",
			messageIndex: index
		});
		return;
	}
	out.push({
		...msg,
		content: dedupedContent
	});
}
/**
* Process one user message: dedup mode-5 duplicate tool_results, strip
* mode-3 orphans whose tool_call is missing from the previously-emitted
* assistant message, and prepend any synthetic results queued by the prior
* assistant's mode-1 repair.
*/
function processUserMessage(args) {
	const { index, msg, pendingOrphansByUserIndex, out, fireRepair, markChanged } = args;
	const queuedOrphans = pendingOrphansByUserIndex.get(index);
	pendingOrphansByUserIndex.delete(index);
	const prev = out.length > 0 ? out[out.length - 1] : null;
	const validIds = prev && prev.role === "assistant" ? collectIds(prev.content, "tool_call", (b) => b.id) : /* @__PURE__ */ new Set();
	const seenCallIds = /* @__PURE__ */ new Set();
	let modifiedHere = false;
	const afterDedup = [];
	for (const block of msg.content) {
		if (block.type !== "tool_result") {
			afterDedup.push(block);
			continue;
		}
		if (seenCallIds.has(block.callId)) {
			modifiedHere = true;
			fireRepair({
				mode: "duplicate-tool-result-strip",
				callId: block.callId,
				messageIndex: index
			});
			continue;
		}
		seenCallIds.add(block.callId);
		afterDedup.push(block);
	}
	const afterStrip = [];
	for (const block of afterDedup) {
		if (block.type === "tool_result" && !validIds.has(block.callId)) {
			modifiedHere = true;
			fireRepair({
				mode: "orphan-tool-result-strip",
				callId: block.callId,
				messageIndex: index
			});
			continue;
		}
		afterStrip.push(block);
	}
	const finalContent = queuedOrphans ? [...queuedOrphans.map(syntheticResultBlock), ...afterStrip] : afterStrip;
	if (!modifiedHere && queuedOrphans === void 0) {
		out.push(msg);
		return;
	}
	markChanged();
	if (finalContent.length === 0) {
		out.push({
			...msg,
			content: [{
				type: "text",
				text: ORPHANED_TOOL_RESULT_MARKER
			}]
		});
		return;
	}
	out.push({
		...msg,
		content: finalContent
	});
}
/**
* Re-mint tool-call ids that collide with ids already present in the
* transcript (or with an earlier sibling in the same turn), BEFORE the turn
* is persisted and its tools dispatched.
*
* Why this exists: some providers/serving stacks emit non-globally-unique
* tool_use ids (per-turn counters like `call_0`, retry-replay duplicates,
* Kimi-K2.6 on Fireworks). The wire-time pairing pass
* ({@link ensureToolResultPairing}) can only STRIP such duplicates — which
* erases the model's tool calls and results from the wire view, stalls
* progress, and re-accumulates repairs quadratically because the persisted
* session keeps the corrupted ids. Re-minting at persist time keeps the
* pairing intact instead: the call, its execution, and its tool_result all
* carry the fresh id, so the pairing pass has nothing to strip on any later
* turn.
*
* Fresh ids are `${oldId}_r{n}` with `n` bumped until unique — deterministic
* shape, Anthropic-wire-safe charset (given a safe input id), and visibly
* traceable back to the original.
*
* Alignment contract: `toolCalls` and the `tool_call` blocks inside
* `content` are two views of the same emission and MUST be in the same
* order (true for every provider — both are built from one source array).
* Occurrences are matched positionally per old id so intra-turn duplicates
* map to distinct fresh ids on both views.
*
* Pure: returns the input references unchanged when no id collides.
*/
function remintDuplicateToolCallIds(toolCalls, content, priorIds) {
	const seen = new Set(priorIds);
	const reminted = [];
	const finalIdQueues = /* @__PURE__ */ new Map();
	let changed = false;
	const outCalls = toolCalls.map((tc) => {
		let finalId = tc.id;
		if (seen.has(tc.id)) {
			let n = 1;
			while (seen.has(`${tc.id}_r${n}`)) n++;
			finalId = `${tc.id}_r${n}`;
			reminted.push({
				oldId: tc.id,
				newId: finalId
			});
			changed = true;
		}
		seen.add(finalId);
		const queue = finalIdQueues.get(tc.id);
		if (queue) queue.push(finalId);
		else finalIdQueues.set(tc.id, [finalId]);
		return finalId === tc.id ? tc : {
			...tc,
			id: finalId
		};
	});
	if (!changed) return {
		toolCalls,
		content,
		reminted
	};
	return {
		toolCalls: outCalls,
		content: content.map((block) => {
			if (block.type !== "tool_call") return block;
			const finalId = finalIdQueues.get(block.id)?.shift();
			if (finalId === void 0 || finalId === block.id) return block;
			return {
				...block,
				id: finalId
			};
		}),
		reminted
	};
}
function syntheticResultBlock(callId) {
	return {
		type: "tool_result",
		callId,
		output: SYNTHETIC_TOOL_RESULT_PLACEHOLDER,
		isError: true
	};
}
function collectResultIds(content) {
	const ids = /* @__PURE__ */ new Set();
	for (const block of content) if (block.type === "tool_result") ids.add(block.callId);
	return ids;
}
function collectIds(content, type, getId) {
	const ids = /* @__PURE__ */ new Set();
	for (const block of content) if (block.type === type) ids.add(getId(block));
	return ids;
}
/**
* Drop ASSISTANT turns whose every `tool_call` block is unresolved
* (no matching `tool_result` block anywhere later in the transcript).
* Tool_call blocks with at least one matching tool_result are kept — modes
* 1/3 of {@link ensureToolResultPairing} handle the partial-pair case at
* wire-send time.
*
* Use case: session resume. A turn that emitted three `tool_use` blocks but
* never persisted the matching `tool_result` turn (process death, crash,
* `kill -9`) leaves the transcript with an orphan that Anthropic rejects on
* the FIRST API call after reload. Dropping the whole assistant turn — not
* just the orphan blocks — preserves text-only turns that legitimately
* carry reasoning the model wants to see again.
*
* Does NOT mint fresh ids: re-id'ing on every resume would cause
* exponential transcript growth across repeated resumes of an interrupted
* session.
*
* Pure: returns the input reference unchanged when no turn was dropped.
*/
function filterUnresolvedToolUses(turns) {
	if (turns.length === 0) return turns;
	const resolvedIds = /* @__PURE__ */ new Set();
	for (const turn of turns) for (const block of turn.content) if (block.type === "tool_result") resolvedIds.add(block.callId);
	let changed = false;
	const out = [];
	for (const turn of turns) {
		if (turn.role !== "assistant") {
			out.push(turn);
			continue;
		}
		const toolCalls = turn.content.filter((b) => b.type === "tool_call");
		if (toolCalls.length === 0) {
			out.push(turn);
			continue;
		}
		if (toolCalls.every((b) => b.type === "tool_call" && !resolvedIds.has(b.id))) {
			changed = true;
			continue;
		}
		out.push(turn);
	}
	return changed ? out : turns;
}
/**
* Inspect a session's trailing turn to classify whether the run was
* interrupted mid-tool-call. Pure / synchronous — does not modify the input.
*
* Pair with {@link filterUnresolvedToolUses} on the resume path: filter
* first so the result reflects the post-cleanup state (a turn whose orphans
* were stripped reads as 'completed', not 'interrupted').
*/
function detectTurnInterruption(turns) {
	if (turns.length === 0) return "clean";
	const last = turns[turns.length - 1];
	if (last.role === "user") return "clean";
	if (last.role !== "assistant") return "clean";
	const resolvedIds = /* @__PURE__ */ new Set();
	for (const turn of turns) for (const block of turn.content) if (block.type === "tool_result") resolvedIds.add(block.callId);
	for (const block of last.content) if (block.type === "tool_call" && !resolvedIds.has(block.id)) return "interrupted";
	return "completed";
}
/**
* Default directive injected by {@link ensureEndsWithUserMessage} when the
* caller doesn't pass one. Neutral enough to read sensibly in any context —
* compaction passes its own directive, the agent loop falls back to this.
*
* The trailing newline is omitted on purpose; provider `userMessage()`
* helpers wrap the content in the right block shape and any trailing
* whitespace would land in the wire payload as-is.
*/
const DEFAULT_USER_TAIL_DIRECTIVE = "Continue.";
/**
* Guarantee a message list ends with a `role: 'user'` message, appending a
* synthetic directive when it doesn't.
*
* Why: Anthropic's API (and several OpenAI-compat reasoning models — opus
* 4.7, the o-series translation layers on Bedrock/Vertex) rejects requests
* whose final message is `role: 'assistant'` with
* "This model does not support assistant message prefill. The conversation
* must end with a user message."
*
* Triggers in zidane:
*   - Compaction over a session whose last turn is the agent's response
*     (the common case — autocompact fires right after `agent.run()`).
*   - Schema enforcement after an assistant text turn (no `tool_use`, so
*     `ensureToolResultPairing` doesn't synthesize a user tail).
*   - `context:transform` hooks that strip the trailing user turn.
*
* The pair-repair pass {@link ensureToolResultPairing} already covers the
* orphan-`tool_use` case via mode 2; this helper closes the plain-text
* assistant-tail case. They compose: run pair-repair first, then this
* pass, so an assistant turn with orphan tool_uses gets its synthetic
* `tool_result` user turn AND this pass is a no-op (the synthetic turn
* is already user-role).
*
* Empty input passes through unchanged — the caller's "nothing to send"
* guard is responsible for that case; we don't materialize a conversation
* out of an empty list.
*
* Idempotent: returns the input reference unchanged when the list is
* already user-terminated (or empty).
*
* @param messages   Wire-ready message list.
* @param provider   Provider whose `userMessage()` shape we use so the
*                   synthetic turn matches the wire format the rest of
*                   the list already follows.
* @param directive  Text to put in the synthetic user message. Defaults
*                   to {@link DEFAULT_USER_TAIL_DIRECTIVE} ("Continue.").
*                   Compaction overrides this with a summary-specific cue.
*/
function ensureEndsWithUserMessage(messages, provider, directive = DEFAULT_USER_TAIL_DIRECTIVE) {
	if (messages.length === 0) return messages;
	if (messages[messages.length - 1].role === "user") return messages;
	return [...messages, provider.userMessage(directive)];
}
/**
* Build a wire-ready `SessionMessage[]` from raw persisted `SessionTurn[]`.
*
* **The canonical "I want to send these to a provider" projection.** Use
* this — not raw `session.turns` / `store.getTurns()` — when constructing a
* provider request outside `agent.run()`. The agent's own wire pipeline
* (`executeTurn` in `src/loop.ts`) applies the same repair just-in-time;
* downstream consumers that bypass the loop hit `tool_result must be
* preceded by a tool_call` 400s precisely because they shipped raw turns.
*
* Pipeline:
*   1. Filter `system`-role turns (the wire only carries user/assistant).
*   2. {@link ensureToolResultPairing} — repairs all six orphan / dup
*      corruption modes by inserting synthetic placeholders rather than
*      dropping (preserves the model's tool_use shape).
*   3. {@link ensureEndsWithUserMessage} when `options.provider` is set —
*      guards against assistant-tail prefill rejection (opus 4.7, o-series).
*
* Pure + idempotent. Does not mutate the input turns. Re-running on the
* output is a no-op.
*
* **Do not feed the result back into `session.setTurns` / `appendTurns`.**
* The repair can insert `SYNTHETIC_TOOL_RESULT_PLACEHOLDER` / `[Orphaned
* tool result removed …]` markers; round-tripping those into persisted
* state contaminates the session's reasoning history and defeats the
* "don't rewrite the past" invariant the wire-only repair was designed
* around.
*/
function toWireMessages(turns, options = {}) {
	const messages = [];
	for (const t of turns) {
		if (t.role === "system") continue;
		messages.push({
			role: t.role,
			content: t.content
		});
	}
	const paired = ensureToolResultPairing(messages, options.onRepair ? { onRepair: options.onRepair } : {});
	if (!options.provider) return paired;
	return ensureEndsWithUserMessage(paired, options.provider, options.userTailDirective);
}
function autoDetectAndConvert(msg) {
	const c = msg.content;
	if (c && typeof c === "object" && !Array.isArray(c)) {
		const tag = c._tag;
		if (typeof tag === "string" && tag.startsWith("__zidane_")) return fromOpenAI(msg);
	}
	if (Array.isArray(c)) {
		for (const block of c) {
			if (!block || typeof block !== "object") continue;
			const b = block;
			if (b.type === "tool_use" || b.type === "tool_result" && "tool_use_id" in b) return fromAnthropic(msg);
			if (b.type === "image_url") return fromOpenAI(msg);
		}
		return fromAnthropic(msg);
	}
	if (typeof c === "string") return fromAnthropic(msg);
	return fromAnthropic(msg);
}
//#endregion
export { renderSystemForWire as A, sanitizeToolSpecs as C, appendStaticSection as D, appendDynamicSection as E, splitSystemPrompt as M, hasSystemPromptBoundary as O, sanitizeToolSchema as S, SYSTEM_PROMPT_BOUNDARY as T, classifyOpenAICompatError as _, detectTurnInterruption as a, toolResultsMessage as b, filterUnresolvedToolUses as c, remintDuplicateToolCallIds as d, toAnthropic as f, assistantMessage as g, OpenAICompatHttpError as h, autoDetectAndConvert as i, replaceDynamicSection as j, joinSystemPrompt as k, fromAnthropic as l, toWireMessages as m, SYNTHETIC_TOOL_RESULT_PLACEHOLDER as n, ensureEndsWithUserMessage as o, toOpenAI as p, TOOL_USE_INTERRUPTED_MARKER as r, ensureToolResultPairing as s, ORPHANED_TOOL_RESULT_MARKER as t, fromOpenAI as u, mapOAIFinishReason as v, fillEstimatedCost as w, userMessage as x, openaiCompat as y };

//# sourceMappingURL=messages-C_1AmSpk.js.map