@copilotkit/aimock 1.29.0 → 1.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/agui-types.d.ts.map +1 -1
- package/dist/bedrock-converse.cjs +63 -31
- package/dist/bedrock-converse.cjs.map +1 -1
- package/dist/bedrock-converse.d.cts.map +1 -1
- package/dist/bedrock-converse.d.ts.map +1 -1
- package/dist/bedrock-converse.js +65 -33
- package/dist/bedrock-converse.js.map +1 -1
- package/dist/bedrock.cjs +95 -33
- package/dist/bedrock.cjs.map +1 -1
- package/dist/bedrock.d.cts.map +1 -1
- package/dist/bedrock.d.ts.map +1 -1
- package/dist/bedrock.js +97 -35
- package/dist/bedrock.js.map +1 -1
- package/dist/cohere.cjs +49 -15
- package/dist/cohere.cjs.map +1 -1
- package/dist/cohere.d.cts.map +1 -1
- package/dist/cohere.d.ts.map +1 -1
- package/dist/cohere.js +51 -17
- package/dist/cohere.js.map +1 -1
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/elevenlabs-audio.cjs +8 -4
- package/dist/elevenlabs-audio.cjs.map +1 -1
- package/dist/elevenlabs-audio.d.cts.map +1 -1
- package/dist/elevenlabs-audio.d.ts.map +1 -1
- package/dist/elevenlabs-audio.js +10 -6
- package/dist/elevenlabs-audio.js.map +1 -1
- package/dist/embeddings.cjs +4 -3
- package/dist/embeddings.cjs.map +1 -1
- package/dist/embeddings.d.cts.map +1 -1
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/embeddings.js +6 -5
- package/dist/embeddings.js.map +1 -1
- package/dist/fal-audio.cjs +8 -4
- package/dist/fal-audio.cjs.map +1 -1
- package/dist/fal-audio.d.cts.map +1 -1
- package/dist/fal-audio.d.ts.map +1 -1
- package/dist/fal-audio.js +10 -6
- package/dist/fal-audio.js.map +1 -1
- package/dist/fal.cjs +4 -2
- package/dist/fal.cjs.map +1 -1
- package/dist/fal.d.cts.map +1 -1
- package/dist/fal.d.ts.map +1 -1
- package/dist/fal.js +6 -4
- package/dist/fal.js.map +1 -1
- package/dist/gemini-embeddings.cjs +4 -3
- package/dist/gemini-embeddings.cjs.map +1 -1
- package/dist/gemini-embeddings.js +6 -5
- package/dist/gemini-embeddings.js.map +1 -1
- package/dist/gemini-interactions.cjs +3 -3
- package/dist/gemini-interactions.cjs.map +1 -1
- package/dist/gemini-interactions.d.cts.map +1 -1
- package/dist/gemini-interactions.d.ts.map +1 -1
- package/dist/gemini-interactions.js +5 -5
- package/dist/gemini-interactions.js.map +1 -1
- package/dist/gemini.cjs +55 -24
- package/dist/gemini.cjs.map +1 -1
- package/dist/gemini.d.cts.map +1 -1
- package/dist/gemini.d.ts.map +1 -1
- package/dist/gemini.js +57 -26
- package/dist/gemini.js.map +1 -1
- package/dist/helpers.cjs +120 -2
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts +43 -3
- package/dist/helpers.d.cts.map +1 -1
- package/dist/helpers.d.ts +43 -3
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +117 -3
- package/dist/helpers.js.map +1 -1
- package/dist/images.cjs +12 -6
- package/dist/images.cjs.map +1 -1
- package/dist/images.d.cts.map +1 -1
- package/dist/images.d.ts.map +1 -1
- package/dist/images.js +14 -8
- package/dist/images.js.map +1 -1
- package/dist/index.cjs +3 -0
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/messages.cjs +325 -85
- package/dist/messages.cjs.map +1 -1
- package/dist/messages.d.cts.map +1 -1
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +327 -87
- package/dist/messages.js.map +1 -1
- package/dist/model-utils.cjs +68 -0
- package/dist/model-utils.cjs.map +1 -1
- package/dist/model-utils.js +68 -1
- package/dist/model-utils.js.map +1 -1
- package/dist/ollama.cjs +58 -21
- package/dist/ollama.cjs.map +1 -1
- package/dist/ollama.d.cts.map +1 -1
- package/dist/ollama.d.ts.map +1 -1
- package/dist/ollama.js +60 -23
- package/dist/ollama.js.map +1 -1
- package/dist/recorder.cjs +49 -8
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.js +50 -9
- package/dist/recorder.js.map +1 -1
- package/dist/responses.cjs +26 -12
- package/dist/responses.cjs.map +1 -1
- package/dist/responses.d.cts +1 -1
- package/dist/responses.d.cts.map +1 -1
- package/dist/responses.d.ts +1 -1
- package/dist/responses.d.ts.map +1 -1
- package/dist/responses.js +28 -14
- package/dist/responses.js.map +1 -1
- package/dist/router.cjs +37 -8
- package/dist/router.cjs.map +1 -1
- package/dist/router.d.cts +30 -1
- package/dist/router.d.cts.map +1 -1
- package/dist/router.d.ts +30 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +37 -9
- package/dist/router.js.map +1 -1
- package/dist/server.cjs +15 -9
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +17 -11
- package/dist/server.js.map +1 -1
- package/dist/speech.cjs +4 -2
- package/dist/speech.cjs.map +1 -1
- package/dist/speech.d.cts.map +1 -1
- package/dist/speech.d.ts.map +1 -1
- package/dist/speech.js +6 -4
- package/dist/speech.js.map +1 -1
- package/dist/stream-collapse.cjs +44 -1
- package/dist/stream-collapse.cjs.map +1 -1
- package/dist/stream-collapse.d.cts +28 -0
- package/dist/stream-collapse.d.cts.map +1 -1
- package/dist/stream-collapse.d.ts +28 -0
- package/dist/stream-collapse.d.ts.map +1 -1
- package/dist/stream-collapse.js +44 -2
- package/dist/stream-collapse.js.map +1 -1
- package/dist/transcription.cjs +4 -2
- package/dist/transcription.cjs.map +1 -1
- package/dist/transcription.d.cts.map +1 -1
- package/dist/transcription.d.ts.map +1 -1
- package/dist/transcription.js +6 -4
- package/dist/transcription.js.map +1 -1
- package/dist/types.d.cts +42 -0
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +42 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vector-types.d.cts.map +1 -1
- package/dist/vector-types.d.ts.map +1 -1
- package/dist/video.cjs +4 -2
- package/dist/video.cjs.map +1 -1
- package/dist/video.d.cts.map +1 -1
- package/dist/video.d.ts.map +1 -1
- package/dist/video.js +6 -4
- package/dist/video.js.map +1 -1
- package/dist/ws-gemini-live.cjs +4 -3
- package/dist/ws-gemini-live.cjs.map +1 -1
- package/dist/ws-gemini-live.d.cts.map +1 -1
- package/dist/ws-gemini-live.d.ts.map +1 -1
- package/dist/ws-gemini-live.js +6 -5
- package/dist/ws-gemini-live.js.map +1 -1
- package/dist/ws-realtime.cjs +4 -3
- package/dist/ws-realtime.cjs.map +1 -1
- package/dist/ws-realtime.d.cts.map +1 -1
- package/dist/ws-realtime.d.ts.map +1 -1
- package/dist/ws-realtime.js +6 -5
- package/dist/ws-realtime.js.map +1 -1
- package/dist/ws-responses.cjs +8 -6
- package/dist/ws-responses.cjs.map +1 -1
- package/dist/ws-responses.d.cts.map +1 -1
- package/dist/ws-responses.d.ts.map +1 -1
- package/dist/ws-responses.js +10 -8
- package/dist/ws-responses.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream-collapse.js","names":[],"sources":["../src/stream-collapse.ts"],"sourcesContent":["/**\n * Stream collapsing functions for record-and-replay.\n *\n * Each function takes a raw streaming response body (SSE, NDJSON, or binary\n * EventStream) and collapses it into a non-streaming fixture response\n * containing `{ content }`, `{ toolCalls }`, or both when the stream includes\n * text followed by tool calls.\n */\n\nimport { crc32 } from \"node:zlib\";\nimport type { RecordProviderKey, ToolCall } from \"./types.js\";\nimport type { Logger } from \"./logger.js\";\nimport { isHarmonyContent, parseHarmonyContent } from \"./harmony.js\";\n\n// ---------------------------------------------------------------------------\n// Result type shared by all collapse functions\n// ---------------------------------------------------------------------------\n\nexport interface CollapseResult {\n content?: string;\n reasoning?: string;\n webSearches?: string[];\n toolCalls?: ToolCall[];\n droppedChunks?: number;\n firstDroppedSample?: string;\n truncated?: boolean;\n audioB64?: string;\n audioMimeType?: string;\n /**\n * Set when harmony channel tokens were present in the accumulated content but\n * could NOT be parsed into a complete, valid harmony structure. The content\n * is preserved VERBATIM, so this is NOT transport loss — it is distinct from\n * `droppedChunks` / `truncated`, which are reserved for genuine transport loss\n * (malformed SSE/NDJSON frames, CRC mismatch). The caller surfaces this as a\n * dedicated warning rather than a dropped/truncated-chunk warning.\n */\n harmonyUnparsed?: true;\n /** Short human-readable note accompanying {@link harmonyUnparsed}. */\n harmonyNote?: string;\n}\n\n/**\n * Slice the first `max` UTF-16 code units of `s` for a diagnostic sample,\n * trimming a trailing lone high-surrogate so the resulting sample never ends on\n * a lone high surrogate (i.e. never mid-surrogate-pair).\n */\nfunction surrogateSafeSlice(s: string, max: number): string {\n let out = s.slice(0, max);\n if (out.length > 0) {\n const last = out.charCodeAt(out.length - 1);\n // A high surrogate (U+D800..U+DBFF) at the end is the lead of a split pair.\n if (last >= 0xd800 && last <= 0xdbff) {\n out = out.slice(0, -1);\n }\n }\n return out;\n}\n\n/**\n * Split a raw SSE body into per-event blocks.\n *\n * Events are delimited by a blank line. Real HTTP/SSE transports use CRLF\n * (`\\r\\n`) line endings, so the inter-event delimiter is `\\r\\n\\r\\n` (which\n * contains no `\\n\\n` substring) and each line ends with a trailing `\\r`.\n * Splitting on `/\\r?\\n\\r?\\n/` handles LF, CRLF, and mixed streams; per-line\n * `\\r` trimming happens in {@link splitSSELines}. Blank blocks are dropped.\n */\nfunction splitSSEEvents(body: string): string[] {\n return body.split(/\\r?\\n\\r?\\n/).filter((block) => block.trim().length > 0);\n}\n\n/**\n * Split a single SSE event block into its lines, trimming a trailing `\\r` so\n * CRLF streams parse identically to LF streams.\n */\nfunction splitSSELines(block: string): string[] {\n return block.split(\"\\n\").map((line) => (line.endsWith(\"\\r\") ? line.slice(0, -1) : line));\n}\n\n/**\n * Extract the SSE `data` field from a single event block's lines.\n *\n * Per the SSE spec a single event may carry MULTIPLE `data:` lines; the field\n * value is every data line's content joined with \"\\n\". Collecting only the\n * first `data:` line (e.g. via `.find`) corrupts payloads that a server split\n * across lines. Callers MUST pass lines produced by {@link splitSSELines} so\n * any trailing `\\r` is already stripped. Returns the joined payload (with the\n * leading \"data:\" prefix and one optional leading space stripped per line), or\n * `undefined` when the block contains no `data:` line.\n */\nfunction extractSSEData(lines: string[]): string | undefined {\n const dataParts: string[] = [];\n for (const line of lines) {\n if (!line.startsWith(\"data:\")) continue;\n // Strip \"data:\" then a single optional leading space, per the SSE spec.\n let part = line.slice(5);\n if (part.startsWith(\" \")) part = part.slice(1);\n dataParts.push(part);\n }\n if (dataParts.length === 0) return undefined;\n return dataParts.join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// 1. OpenAI SSE\n// ---------------------------------------------------------------------------\n\n/**\n * Collapse OpenAI Chat Completions SSE stream into a single response.\n *\n * Format:\n * data: {\"id\":\"chatcmpl-123\",\"choices\":[{\"delta\":{\"content\":\"Hello\"}}]}\\n\\n\n * data: [DONE]\\n\\n\n */\nexport function collapseOpenAISSE(body: string): CollapseResult {\n const lines = splitSSEEvents(body);\n let content = \"\";\n let reasoning = \"\";\n const webSearchQueries: string[] = [];\n let droppedChunks = 0;\n let firstDroppedSample: string | undefined;\n let harmonyUnparsed = false;\n let harmonyNote: string | undefined;\n const toolCallMap = new Map<number, { id: string; name: string; arguments: string }>();\n // Fallback keying for deltas that OMIT `index`. Without this, every\n // index-less delta collapses under one `undefined`/NaN key, merging distinct\n // tool calls and corrupting arguments. Index-less fragments that share an\n // `id` correlate via `idKeyMap`; otherwise each gets a fresh synthetic key\n // assigned from a counter kept above any real index so sort order is stable.\n // The 1_000_000 sentinel assumes real provider tool-call indices stay below\n // it (they are small per-stream counters), so synthetic keys never collide.\n let nextSyntheticIndex = 1_000_000;\n const idKeyMap = new Map<string, number>();\n\n for (const line of lines) {\n const data = extractSSEData(splitSSELines(line));\n if (data === undefined) continue;\n\n const payload = data.trim();\n if (payload === \"[DONE]\") continue;\n\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(payload) as Record<string, unknown>;\n } catch (err) {\n droppedChunks++;\n if (droppedChunks === 1) {\n const msg = err instanceof Error ? err.message : \"unknown\";\n firstDroppedSample = `parse failed (${msg}): ${surrogateSafeSlice(payload, 200)}`;\n }\n continue;\n }\n\n // Responses API reasoning events\n if (\n parsed.type === \"response.reasoning_summary_text.delta\" &&\n typeof parsed.delta === \"string\"\n ) {\n reasoning += parsed.delta;\n continue;\n }\n\n // Responses API web search events\n if (parsed.type === \"response.output_item.done\") {\n const item = parsed.item as Record<string, unknown> | undefined;\n if (item?.type === \"web_search_call\") {\n const action = item.action as Record<string, unknown> | undefined;\n if (action && typeof action.query === \"string\") {\n webSearchQueries.push(action.query);\n continue;\n }\n }\n }\n\n // Responses API text content events\n if (parsed.type === \"response.output_text.delta\" && typeof parsed.delta === \"string\") {\n content += parsed.delta;\n continue;\n }\n\n // Skip other Responses API structural events\n if (typeof parsed.type === \"string\" && parsed.type.startsWith(\"response.\")) {\n continue;\n }\n\n const choices = parsed.choices as Array<Record<string, unknown>> | undefined;\n if (!choices || choices.length === 0) continue;\n\n const delta = choices[0].delta as Record<string, unknown> | undefined;\n if (!delta) continue;\n\n // Reasoning content (OpenRouter / chat completions format)\n if (typeof delta.reasoning_content === \"string\") {\n reasoning += delta.reasoning_content;\n }\n\n // Text content\n if (typeof delta.content === \"string\") {\n content += delta.content;\n }\n\n // Tool calls\n const toolCalls = delta.tool_calls as Array<Record<string, unknown>> | undefined;\n if (toolCalls) {\n for (const tc of toolCalls) {\n const fn = tc.function as Record<string, unknown> | undefined;\n const rawId = typeof tc.id === \"string\" ? tc.id : undefined;\n\n // Resolve a stable map key. Prefer the streamed `index`; when it is\n // absent, correlate by `id` if present, else mint a fresh synthetic\n // key so distinct index-less calls never merge.\n let index: number;\n if (typeof tc.index === \"number\") {\n index = tc.index;\n } else if (rawId !== undefined) {\n const existing = idKeyMap.get(rawId);\n if (existing !== undefined) {\n index = existing;\n } else {\n index = nextSyntheticIndex++;\n idKeyMap.set(rawId, index);\n }\n } else {\n index = nextSyntheticIndex++;\n }\n\n if (!toolCallMap.has(index)) {\n toolCallMap.set(index, {\n id: rawId ?? \"\",\n name: (fn?.name as string) ?? \"\",\n arguments: \"\",\n });\n }\n\n const entry = toolCallMap.get(index)!;\n if (fn?.name && typeof fn.name === \"string\" && !entry.name) {\n entry.name = fn.name;\n }\n if (tc.id && typeof tc.id === \"string\" && !entry.id) {\n entry.id = tc.id;\n }\n if (fn?.arguments && typeof fn.arguments === \"string\") {\n entry.arguments += fn.arguments;\n }\n }\n }\n }\n\n // Open-weight gpt-oss models (Ollama / vLLM / OpenRouter) stream tool calls\n // as raw harmony channel tokens inside delta.content rather than structured\n // delta.tool_calls. Harmony parsing is FALLBACK-ONLY: attempt it ONLY when\n // there are NO structured delta.tool_calls. If structured tool calls exist,\n // any harmony-looking content is prose — never merged (no phantom tool call),\n // never stamped as truncated/dropped. When harmony IS the only source, a\n // successful parse routes channels (content/reasoning/toolCalls); a failure\n // preserves content VERBATIM and surfaces the distinct `harmonyUnparsed`\n // signal (NOT droppedChunks/truncated — the bytes are not lost).\n const harmonyToolCalls: ToolCall[] = [];\n if (toolCallMap.size === 0 && isHarmonyContent(content)) {\n const parsed = parseHarmonyContent(content);\n if (parsed.failed) {\n harmonyUnparsed = true;\n harmonyNote = `harmony tokens present but unparseable; content preserved verbatim: ${surrogateSafeSlice(content, 200)}`;\n } else {\n content = parsed.content;\n if (parsed.reasoning) {\n reasoning += parsed.reasoning;\n }\n harmonyToolCalls.push(...parsed.toolCalls);\n }\n }\n\n if (toolCallMap.size > 0 || harmonyToolCalls.length > 0) {\n const sorted = Array.from(toolCallMap.entries()).sort(([a], [b]) => a - b);\n return {\n ...(content ? { content } : {}),\n // Fallback-only: harmonyToolCalls are populated ONLY in the\n // no-structured-calls branch, so this is never a merge of both sources.\n toolCalls: [\n ...sorted.map(([, tc]) => ({\n name: tc.name,\n arguments: tc.arguments,\n ...(tc.id ? { id: tc.id } : {}),\n })),\n ...harmonyToolCalls,\n ],\n // Reasoning is preserved alongside tool calls for ALL structured streams\n // (DeepSeek/OpenRouter reasoning_content, harmony analysis channel), at\n // parity with every other collapser and the non-streaming path.\n ...(reasoning ? { reasoning } : {}),\n // webSearches parity with the text-only return branch.\n ...(webSearchQueries.length > 0 ? { webSearches: webSearchQueries } : {}),\n ...(droppedChunks > 0 ? { droppedChunks } : {}),\n ...(firstDroppedSample ? { firstDroppedSample } : {}),\n ...(harmonyUnparsed ? { harmonyUnparsed: true } : {}),\n ...(harmonyNote ? { harmonyNote } : {}),\n };\n }\n\n return {\n content,\n ...(reasoning ? { reasoning } : {}),\n ...(webSearchQueries.length > 0 ? { webSearches: webSearchQueries } : {}),\n ...(droppedChunks > 0 ? { droppedChunks } : {}),\n ...(firstDroppedSample ? { firstDroppedSample } : {}),\n ...(harmonyUnparsed ? { harmonyUnparsed: true } : {}),\n ...(harmonyNote ? { harmonyNote } : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// 2. Anthropic SSE\n// ---------------------------------------------------------------------------\n\n/**\n * Collapse Anthropic Claude Messages SSE stream into a single response.\n *\n * Format:\n * event: message_start\\ndata: {...}\\n\\n\n * event: content_block_delta\\ndata: {\"delta\":{\"type\":\"text_delta\",\"text\":\"Hello\"}}\\n\\n\n */\nexport function collapseAnthropicSSE(body: string): CollapseResult {\n const blocks = splitSSEEvents(body);\n let content = \"\";\n let reasoning = \"\";\n let droppedChunks = 0;\n let firstDroppedSample: string | undefined;\n const toolCallMap = new Map<number, { id: string; name: string; arguments: string }>();\n // Fallback keying for content blocks that OMIT `index` (mirrors the OpenAI /\n // Cohere / Bedrock guards). Without it, every index-less block collapses\n // under one `undefined` key, merging distinct tool_use blocks. Index-less\n // starts mint a fresh synthetic key (kept above any real index so sort order\n // is stable). Despite its name, `lastSyntheticIndex` tracks whichever\n // tool_use start most recently opened REGARDLESS of whether its index was\n // real or synthetic (it is set on EVERY start), so an index-less delta\n // correlates to the most-recent start — not just to the last synthetic one.\n // The 1_000_000 sentinel assumes real provider indices stay below it.\n let nextSyntheticIndex = 1_000_000;\n let lastSyntheticIndex: number | undefined;\n\n for (const block of blocks) {\n const lines = splitSSELines(block);\n const eventLine = lines.find((l) => l.startsWith(\"event:\"));\n const data = extractSSEData(lines);\n if (data === undefined) continue;\n\n const eventType = eventLine ? eventLine.slice(6).trim() : \"\";\n const payload = data.trim();\n\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(payload) as Record<string, unknown>;\n } catch (err) {\n droppedChunks++;\n if (droppedChunks === 1) {\n const msg = err instanceof Error ? err.message : \"unknown\";\n firstDroppedSample = `parse failed (${msg}): ${surrogateSafeSlice(payload, 200)}`;\n }\n continue;\n }\n\n if (eventType === \"content_block_start\") {\n const contentBlock = parsed.content_block as Record<string, unknown> | undefined;\n if (contentBlock?.type === \"tool_use\") {\n // Prefer the streamed `index`; when absent, mint a fresh synthetic key\n // so distinct index-less tool_use blocks never merge.\n let index: number;\n if (typeof parsed.index === \"number\") {\n index = parsed.index;\n } else {\n index = nextSyntheticIndex++;\n }\n lastSyntheticIndex = index;\n toolCallMap.set(index, {\n id: (contentBlock.id as string) ?? \"\",\n name: (contentBlock.name as string) ?? \"\",\n arguments: \"\",\n });\n }\n }\n\n if (eventType === \"content_block_delta\") {\n const delta = parsed.delta as Record<string, unknown> | undefined;\n if (!delta) continue;\n\n if (delta.type === \"text_delta\" && typeof delta.text === \"string\") {\n content += delta.text;\n }\n\n if (delta.type === \"thinking_delta\" && typeof delta.thinking === \"string\") {\n reasoning += delta.thinking;\n }\n\n if (delta.type === \"input_json_delta\" && typeof delta.partial_json === \"string\") {\n // Use the streamed `index` when present; otherwise correlate to the\n // most recent tool_use start (mirrors the start-side fallback).\n const index = typeof parsed.index === \"number\" ? parsed.index : lastSyntheticIndex;\n // A delta that cannot correlate to any known start (no streamed index\n // AND no prior start, or a stale index with no entry) would otherwise\n // silently lose its args. Account for it as a dropped chunk instead of\n // vanishing (mirrors the Cohere uncorrelated-delta path).\n const entry = index !== undefined ? toolCallMap.get(index) : undefined;\n if (entry) {\n entry.arguments += delta.partial_json;\n } else {\n droppedChunks++;\n if (droppedChunks === 1) {\n firstDroppedSample = `input_json_delta with no correlating tool_use start: ${surrogateSafeSlice(\n payload,\n 200,\n )}`;\n }\n }\n }\n }\n }\n\n if (toolCallMap.size > 0) {\n const sorted = Array.from(toolCallMap.entries()).sort(([a], [b]) => a - b);\n return {\n ...(content ? { content } : {}),\n toolCalls: sorted.map(([, tc]) => ({\n name: tc.name,\n arguments: tc.arguments,\n ...(tc.id ? { id: tc.id } : {}),\n })),\n ...(reasoning ? { reasoning } : {}),\n ...(droppedChunks > 0 ? { droppedChunks } : {}),\n ...(firstDroppedSample ? { firstDroppedSample } : {}),\n };\n }\n\n return {\n content,\n ...(reasoning ? { reasoning } : {}),\n ...(droppedChunks > 0 ? { droppedChunks } : {}),\n ...(firstDroppedSample ? { firstDroppedSample } : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// 3. Gemini SSE\n// ---------------------------------------------------------------------------\n\n/**\n * Collapse Gemini SSE stream into a single response.\n *\n * Format (data-only, no event prefix, no [DONE]):\n * data: {\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"Hello\"}]}}]}\\n\\n\n */\nexport function collapseGeminiSSE(body: string): CollapseResult {\n const lines = splitSSEEvents(body);\n let content = \"\";\n let reasoning = \"\";\n let droppedChunks = 0;\n let firstDroppedSample: string | undefined;\n let audioB64 = \"\";\n let audioMimeType: string | undefined;\n const toolCalls: ToolCall[] = [];\n\n for (const line of lines) {\n const data = extractSSEData(splitSSELines(line));\n if (data === undefined) continue;\n\n const payload = data.trim();\n\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(payload) as Record<string, unknown>;\n } catch (err) {\n droppedChunks++;\n if (droppedChunks === 1) {\n const msg = err instanceof Error ? err.message : \"unknown\";\n firstDroppedSample = `parse failed (${msg}): ${surrogateSafeSlice(payload, 200)}`;\n }\n continue;\n }\n\n const candidates = parsed.candidates as Array<Record<string, unknown>> | undefined;\n if (!candidates || candidates.length === 0) continue;\n\n const candidateContent = candidates[0].content as Record<string, unknown> | undefined;\n if (!candidateContent) continue;\n\n const parts = candidateContent.parts as Array<Record<string, unknown>> | undefined;\n if (!parts || parts.length === 0) continue;\n\n for (const part of parts) {\n if (part.functionCall) {\n const fc = part.functionCall as Record<string, unknown>;\n toolCalls.push({\n name: String(fc.name ?? \"\"),\n // Default undefined/object args to a JSON object string (matches\n // collapseGeminiInteractionsSSE / Ollama). JSON.stringify(undefined)\n // would otherwise yield the VALUE undefined, violating the\n // ToolCall.arguments:string contract.\n arguments:\n typeof fc.args === \"string\" ? (fc.args as string) : JSON.stringify(fc.args ?? {}),\n });\n } else if (\n part.inlineData &&\n typeof (part.inlineData as Record<string, unknown>).mimeType === \"string\" &&\n ((part.inlineData as Record<string, unknown>).mimeType as string).startsWith(\"audio/\")\n ) {\n const inlineData = part.inlineData as Record<string, unknown>;\n if (!audioMimeType) {\n audioMimeType = inlineData.mimeType as string;\n }\n if (typeof inlineData.data === \"string\") {\n audioB64 += inlineData.data;\n }\n } else if (typeof part.text === \"string\") {\n if (part.thought) {\n reasoning += part.text;\n } else {\n content += part.text;\n }\n }\n }\n }\n\n if (audioB64) {\n // Preserve any content / reasoning / tool calls accumulated in the same\n // stream — a Gemini turn can interleave audio with text and functionCall\n // parts, and the early return must not silently drop them.\n return {\n audioB64,\n audioMimeType,\n ...(content ? { content } : {}),\n ...(reasoning ? { reasoning } : {}),\n ...(toolCalls.length > 0 ? { toolCalls } : {}),\n ...(droppedChunks > 0 ? { droppedChunks } : {}),\n ...(firstDroppedSample ? { firstDroppedSample } : {}),\n };\n }\n\n if (toolCalls.length > 0) {\n return {\n ...(content ? { content } : {}),\n toolCalls,\n ...(reasoning ? { reasoning } : {}),\n ...(droppedChunks > 0 ? { droppedChunks } : {}),\n ...(firstDroppedSample ? { firstDroppedSample } : {}),\n };\n }\n\n return {\n content,\n ...(reasoning ? { reasoning } : {}),\n ...(droppedChunks > 0 ? { droppedChunks } : {}),\n ...(firstDroppedSample ? { firstDroppedSample } : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// 4. Ollama NDJSON\n// ---------------------------------------------------------------------------\n\n/**\n * Collapse Ollama NDJSON stream into a single response.\n *\n * /api/chat format:\n * {\"model\":\"llama3\",\"message\":{\"role\":\"assistant\",\"content\":\"Hello\"},\"done\":false}\\n\n *\n * /api/generate format:\n * {\"model\":\"llama3\",\"response\":\"Hello\",\"done\":false}\\n\n *\n * Open-weight gpt-oss served via Ollama streams harmony channel tokens inside\n * `message.content` (just like the OpenAI SSE path), so after accumulation the\n * content is run through the same fail-safe {@link parseHarmonyContent} gate to\n * capture structured tool calls / reasoning instead of leaking raw tokens.\n */\nexport function collapseOllamaNDJSON(body: string): CollapseResult {\n const lines = body.split(\"\\n\").filter((l) => l.trim().length > 0);\n let content = \"\";\n let reasoning = \"\";\n let droppedChunks = 0;\n let firstDroppedSample: string | undefined;\n let harmonyUnparsed = false;\n let harmonyNote: string | undefined;\n const toolCalls: ToolCall[] = [];\n\n for (const line of lines) {\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(line.trim()) as Record<string, unknown>;\n } catch (err) {\n droppedChunks++;\n if (droppedChunks === 1) {\n const msg = err instanceof Error ? err.message : \"unknown\";\n firstDroppedSample = `parse failed (${msg}): ${surrogateSafeSlice(line.trim(), 200)}`;\n }\n continue;\n }\n\n // /api/chat format\n const message = parsed.message as Record<string, unknown> | undefined;\n if (message) {\n if (typeof message.content === \"string\") {\n content += message.content;\n }\n\n // Tool calls\n if (Array.isArray(message.tool_calls)) {\n for (const tc of message.tool_calls as Array<Record<string, unknown>>) {\n const fn = tc.function as Record<string, unknown> | undefined;\n if (fn) {\n toolCalls.push({\n name: String(fn.name ?? \"\"),\n // Default undefined/object args to a JSON object (matching\n // collapseGeminiInteractionsSSE) — JSON.stringify(undefined)\n // would otherwise yield the literal string \"undefined\".\n arguments:\n typeof fn.arguments === \"string\"\n ? fn.arguments\n : JSON.stringify(fn.arguments ?? {}),\n });\n }\n }\n }\n }\n\n // /api/generate format\n else if (typeof parsed.response === \"string\") {\n content += parsed.response;\n }\n }\n\n // Open-weight gpt-oss served via Ollama streams harmony channel tokens inside\n // message.content (same as the OpenAI SSE path). Harmony parsing is\n // FALLBACK-ONLY: attempt it ONLY when there are NO structured message\n // tool_calls. If structured tool calls exist, harmony-looking content is\n // prose — never merged (no phantom), never stamped truncated/dropped. On a\n // harmony failure the content is preserved VERBATIM and surfaced via the\n // distinct `harmonyUnparsed` signal (NOT droppedChunks/truncated).\n if (toolCalls.length === 0 && isHarmonyContent(content)) {\n const parsedHarmony = parseHarmonyContent(content);\n if (parsedHarmony.failed) {\n harmonyUnparsed = true;\n harmonyNote = `harmony tokens present but unparseable; content preserved verbatim: ${surrogateSafeSlice(content, 200)}`;\n } else {\n content = parsedHarmony.content;\n if (parsedHarmony.reasoning) {\n reasoning += parsedHarmony.reasoning;\n }\n toolCalls.push(...parsedHarmony.toolCalls);\n }\n }\n\n if (toolCalls.length > 0) {\n return {\n ...(content ? { content } : {}),\n toolCalls,\n ...(reasoning ? { reasoning } : {}),\n ...(droppedChunks > 0 ? { droppedChunks } : {}),\n ...(firstDroppedSample ? { firstDroppedSample } : {}),\n ...(harmonyUnparsed ? { harmonyUnparsed: true } : {}),\n ...(harmonyNote ? { harmonyNote } : {}),\n };\n }\n\n return {\n content,\n ...(reasoning ? { reasoning } : {}),\n ...(droppedChunks > 0 ? { droppedChunks } : {}),\n ...(firstDroppedSample ? { firstDroppedSample } : {}),\n ...(harmonyUnparsed ? { harmonyUnparsed: true } : {}),\n ...(harmonyNote ? { harmonyNote } : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// 5. Cohere SSE\n// ---------------------------------------------------------------------------\n\n/**\n * Collapse Cohere SSE stream into a single response.\n *\n * Format:\n * event: content-delta\\ndata: {\"type\":\"content-delta\",\"delta\":{\"message\":{\"content\":{\"text\":\"Hello\"}}}}\\n\\n\n */\nexport function collapseCohereSSE(body: string): CollapseResult {\n const blocks = splitSSEEvents(body);\n let content = \"\";\n let droppedChunks = 0;\n let firstDroppedSample: string | undefined;\n const toolCallMap = new Map<number, { id: string; name: string; arguments: string }>();\n // Fallback keying for tool-call events that OMIT `index` (mirrors the\n // OpenAI guard). Without it, every index-less tool-call-start collapses\n // under one `undefined`/NaN key, merging distinct calls. Index-less starts\n // mint a fresh synthetic key. `lastStartKey` tracks the most-recent\n // tool-call-start key REGARDLESS of whether it was real or synthetic, so an\n // index-less tool-call-delta correlates to whichever start most recently\n // opened — not just to the last synthetic one. The 1_000_000 sentinel\n // assumes real provider indices stay below it.\n let nextSyntheticIndex = 1_000_000;\n let lastStartKey: number | undefined;\n\n for (const block of blocks) {\n const lines = splitSSELines(block);\n const eventLine = lines.find((l) => l.startsWith(\"event:\"));\n const data = extractSSEData(lines);\n if (data === undefined) continue;\n\n const eventType = eventLine ? eventLine.slice(6).trim() : \"\";\n const payload = data.trim();\n\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(payload) as Record<string, unknown>;\n } catch (err) {\n droppedChunks++;\n if (droppedChunks === 1) {\n const msg = err instanceof Error ? err.message : \"unknown\";\n firstDroppedSample = `parse failed (${msg}): ${surrogateSafeSlice(payload, 200)}`;\n }\n continue;\n }\n\n if (eventType === \"content-delta\") {\n const delta = parsed.delta as Record<string, unknown> | undefined;\n const message = delta?.message as Record<string, unknown> | undefined;\n const contentObj = message?.content as Record<string, unknown> | undefined;\n if (contentObj && typeof contentObj.text === \"string\") {\n content += contentObj.text;\n }\n }\n\n if (eventType === \"tool-call-start\") {\n let index: number;\n if (typeof parsed.index === \"number\") {\n index = parsed.index;\n } else {\n index = nextSyntheticIndex++;\n }\n // Track the most-recent start key (real OR synthetic) so a following\n // index-less delta correlates to whichever call just opened.\n lastStartKey = index;\n const delta = parsed.delta as Record<string, unknown> | undefined;\n const message = delta?.message as Record<string, unknown> | undefined;\n const toolCalls = message?.tool_calls as Record<string, unknown> | undefined;\n if (toolCalls) {\n const fn = toolCalls.function as Record<string, unknown> | undefined;\n toolCallMap.set(index, {\n id: (toolCalls.id as string) ?? \"\",\n name: (fn?.name as string) ?? \"\",\n arguments: \"\",\n });\n }\n }\n\n if (eventType === \"tool-call-delta\") {\n // Use the streamed `index` when present; otherwise correlate to the most\n // recent tool-call-start (real or synthetic key).\n const index = typeof parsed.index === \"number\" ? parsed.index : lastStartKey;\n const delta = parsed.delta as Record<string, unknown> | undefined;\n const message = delta?.message as Record<string, unknown> | undefined;\n const toolCalls = message?.tool_calls as Record<string, unknown> | undefined;\n if (toolCalls) {\n const fn = toolCalls.function as Record<string, unknown> | undefined;\n if (fn && typeof fn.arguments === \"string\") {\n // A delta that cannot correlate to any known start (no streamed\n // index AND no prior start) would otherwise silently lose its args.\n // Account for it as a dropped chunk instead of vanishing.\n const entry = index !== undefined ? toolCallMap.get(index) : undefined;\n if (entry) {\n entry.arguments += fn.arguments;\n } else {\n droppedChunks++;\n if (droppedChunks === 1) {\n firstDroppedSample = `tool-call-delta with no correlating start: ${surrogateSafeSlice(\n payload,\n 200,\n )}`;\n }\n }\n }\n }\n }\n }\n\n if (toolCallMap.size > 0) {\n const sorted = Array.from(toolCallMap.entries()).sort(([a], [b]) => a - b);\n return {\n ...(content ? { content } : {}),\n toolCalls: sorted.map(([, tc]) => ({\n name: tc.name,\n arguments: tc.arguments,\n ...(tc.id ? { id: tc.id } : {}),\n })),\n ...(droppedChunks > 0 ? { droppedChunks } : {}),\n ...(firstDroppedSample ? { firstDroppedSample } : {}),\n };\n }\n\n return {\n content,\n ...(droppedChunks > 0 ? { droppedChunks } : {}),\n ...(firstDroppedSample ? { firstDroppedSample } : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// 6. Bedrock EventStream (binary)\n// ---------------------------------------------------------------------------\n\n/**\n * Decode AWS Event Stream binary frames and extract JSON payloads.\n *\n * Binary frame layout:\n * [total_length: 4B uint32-BE]\n * [headers_length: 4B uint32-BE]\n * [prelude_crc32: 4B]\n * [headers: variable]\n * [payload: variable]\n * [message_crc32: 4B]\n */\nfunction decodeEventStreamFrames(buf: Buffer): {\n frames: Array<{ headers: Record<string, string>; payload: Buffer }>;\n truncated: boolean;\n} {\n const frames: Array<{ headers: Record<string, string>; payload: Buffer }> = [];\n let offset = 0;\n\n while (offset < buf.length) {\n if (offset + 12 > buf.length) break;\n\n const totalLength = buf.readUInt32BE(offset);\n const headersLength = buf.readUInt32BE(offset + 4);\n\n // Validate bounds: ensure the full frame is within the buffer\n if (totalLength < 12 || offset + totalLength > buf.length) {\n return { frames, truncated: true };\n }\n\n // Validate prelude CRC\n const preludeCrc = buf.readUInt32BE(offset + 8);\n const computedPreludeCrc = crc32(buf.subarray(offset, offset + 8));\n if (preludeCrc >>> 0 !== computedPreludeCrc >>> 0) {\n return { frames, truncated: true }; // Prelude CRC mismatch — stop parsing\n }\n\n // Parse headers\n const headersStart = offset + 12;\n const headersEnd = headersStart + headersLength;\n const payloadEnd = offset + totalLength - 4; // minus message CRC\n\n // Validate the headers region fits inside the frame. A frame can carry a\n // valid prelude CRC yet declare a `headersLength` that overruns the payload\n // region (the prelude CRC only covers total/headers length, not the body).\n // Without this guard a per-header read walks off the buffer and throws an\n // uncaught RangeError; treat it as truncation instead.\n if (headersEnd > payloadEnd || headersEnd > buf.length) {\n return { frames, truncated: true };\n }\n\n const headers: Record<string, string> = {};\n let hOffset = headersStart;\n let headerOverrun = false;\n\n while (hOffset < headersEnd) {\n // Each read must stay within the declared headers region. Bail out\n // (truncated) on any overrun rather than reading past the boundary.\n if (hOffset + 1 > headersEnd) {\n headerOverrun = true;\n break;\n }\n const nameLen = buf.readUInt8(hOffset);\n hOffset += 1;\n if (hOffset + nameLen + 1 + 2 > headersEnd) {\n headerOverrun = true;\n break;\n }\n const name = buf.subarray(hOffset, hOffset + nameLen).toString(\"utf8\");\n hOffset += nameLen;\n // Skip header type byte (type 7 = STRING)\n hOffset += 1;\n const valueLen = buf.readUInt16BE(hOffset);\n hOffset += 2;\n if (hOffset + valueLen > headersEnd) {\n headerOverrun = true;\n break;\n }\n const value = buf.subarray(hOffset, hOffset + valueLen).toString(\"utf8\");\n hOffset += valueLen;\n headers[name] = value;\n }\n\n if (headerOverrun) {\n return { frames, truncated: true };\n }\n\n // Extract payload\n const payloadStart = headersEnd;\n const payload = buf.subarray(payloadStart, payloadEnd);\n\n // Validate message CRC (covers entire frame minus last 4 bytes)\n const messageCrc = buf.readUInt32BE(offset + totalLength - 4);\n const computedMessageCrc = crc32(buf.subarray(offset, offset + totalLength - 4));\n if (messageCrc >>> 0 !== computedMessageCrc >>> 0) {\n return { frames, truncated: true }; // Message CRC mismatch — stop parsing\n }\n\n frames.push({ headers, payload });\n offset += totalLength;\n }\n\n return { frames, truncated: false };\n}\n\n/**\n * Collapse Bedrock binary Event Stream into a single response.\n *\n * Each frame contains a JSON payload with event types like:\n * contentBlockDelta, contentBlockStart, etc.\n */\nexport function collapseBedrockEventStream(body: Buffer): CollapseResult {\n const { frames, truncated } = decodeEventStreamFrames(body);\n let content = \"\";\n let droppedChunks = 0;\n let firstDroppedSample: string | undefined;\n const toolCallMap = new Map<number, { id: string; name: string; arguments: string }>();\n\n for (const frame of frames) {\n const frameStr = frame.payload.toString(\"utf8\");\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(frameStr) as Record<string, unknown>;\n } catch (err) {\n droppedChunks++;\n if (droppedChunks === 1) {\n const msg = err instanceof Error ? err.message : \"unknown\";\n firstDroppedSample = `parse failed (${msg}): ${surrogateSafeSlice(frameStr, 200)}`;\n }\n continue;\n }\n\n // Anthropic Messages format (invoke-with-response-stream): flat payload with \"type\" field\n if (parsed.type === \"content_block_delta\") {\n const delta = parsed.delta as Record<string, unknown> | undefined;\n if (delta?.type === \"text_delta\" && typeof delta.text === \"string\") {\n content += delta.text;\n }\n if (delta?.type === \"input_json_delta\" && typeof delta.partial_json === \"string\") {\n const index = parsed.index as number | undefined;\n // An arg delta that cannot correlate to a known tool_use start would\n // otherwise silently lose its args. Account for it as a dropped chunk\n // instead of vanishing (mirrors the Cohere uncorrelated-delta path).\n const entry = index !== undefined ? toolCallMap.get(index) : undefined;\n if (entry) {\n entry.arguments += delta.partial_json;\n } else {\n droppedChunks++;\n if (droppedChunks === 1) {\n firstDroppedSample = `input_json_delta with no correlating tool_use start: ${surrogateSafeSlice(\n frameStr,\n 200,\n )}`;\n }\n }\n }\n continue;\n }\n if (parsed.type === \"content_block_start\") {\n const block = parsed.content_block as Record<string, unknown> | undefined;\n const index = parsed.index as number | undefined;\n if (block?.type === \"tool_use\" && index !== undefined) {\n toolCallMap.set(index, {\n id: (block.id as string) ?? \"\",\n name: (block.name as string) ?? \"\",\n arguments: \"\",\n });\n }\n continue;\n }\n\n // Converse format (converse-stream): camelCase wrapper keys\n // contentBlockStart — may initiate a tool_use block\n if (parsed.contentBlockStart) {\n const blockStart = parsed.contentBlockStart as Record<string, unknown>;\n const index = (parsed.contentBlockIndex ?? blockStart.contentBlockIndex) as\n | number\n | undefined;\n const start = blockStart.start as Record<string, unknown> | undefined;\n if (start?.toolUse && index !== undefined) {\n const toolUse = start.toolUse as Record<string, unknown>;\n toolCallMap.set(index, {\n id: (toolUse.toolUseId as string) ?? \"\",\n name: (toolUse.name as string) ?? \"\",\n arguments: \"\",\n });\n }\n }\n\n // contentBlockDelta\n if (parsed.contentBlockDelta) {\n const blockDelta = parsed.contentBlockDelta as Record<string, unknown>;\n const index = (parsed.contentBlockIndex ?? blockDelta.contentBlockIndex) as\n | number\n | undefined;\n const delta = blockDelta.delta as Record<string, unknown> | undefined;\n if (!delta) continue;\n\n // Text delta\n if (typeof delta.text === \"string\") {\n content += delta.text;\n }\n\n // Tool use input JSON delta\n if (typeof delta.toolUse === \"object\" && delta.toolUse !== null) {\n const toolUseDelta = delta.toolUse as Record<string, unknown>;\n if (typeof toolUseDelta.input === \"string\") {\n // An arg delta that cannot correlate to a known tool_use start would\n // otherwise silently lose its args. Account for it as a dropped chunk\n // instead of vanishing (mirrors the Cohere uncorrelated-delta path).\n const entry = index !== undefined ? toolCallMap.get(index) : undefined;\n if (entry) {\n entry.arguments += toolUseDelta.input;\n } else {\n droppedChunks++;\n if (droppedChunks === 1) {\n firstDroppedSample = `toolUse.input delta with no correlating tool_use start: ${surrogateSafeSlice(\n frameStr,\n 200,\n )}`;\n }\n }\n }\n }\n }\n }\n\n if (toolCallMap.size > 0) {\n const sorted = Array.from(toolCallMap.entries()).sort(([a], [b]) => a - b);\n return {\n toolCalls: sorted.map(([, tc]) => ({\n name: tc.name,\n arguments: tc.arguments,\n ...(tc.id ? { id: tc.id } : {}),\n })),\n ...(droppedChunks > 0 ? { droppedChunks } : {}),\n ...(firstDroppedSample ? { firstDroppedSample } : {}),\n ...(truncated ? { truncated } : {}),\n };\n }\n\n return {\n content,\n ...(droppedChunks > 0 ? { droppedChunks } : {}),\n ...(firstDroppedSample ? { firstDroppedSample } : {}),\n ...(truncated ? { truncated } : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// 7. Gemini Interactions SSE\n// ---------------------------------------------------------------------------\n\n/**\n * Collapse Gemini Interactions SSE stream into a single response.\n *\n * Format (data-only, event_type inside JSON):\n * data: {\"event_type\":\"content.delta\",\"index\":0,\"delta\":{\"type\":\"text\",\"text\":\"Hello\"}}\\n\\n\n * data: {\"event_type\":\"interaction.complete\",\"interaction\":{\"id\":\"...\",\"usage\":{...}}}\\n\\n\n */\nexport function collapseGeminiInteractionsSSE(body: string): CollapseResult {\n const lines = splitSSEEvents(body);\n let content = \"\";\n let reasoning = \"\";\n let droppedChunks = 0;\n let firstDroppedSample: string | undefined;\n const toolCalls: ToolCall[] = [];\n\n for (const line of lines) {\n const data = extractSSEData(splitSSELines(line));\n if (data === undefined) continue;\n\n const payload = data.trim();\n\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(payload) as Record<string, unknown>;\n } catch (err) {\n droppedChunks++;\n if (droppedChunks === 1) {\n const msg = err instanceof Error ? err.message : \"unknown\";\n firstDroppedSample = `parse failed (${msg}): ${surrogateSafeSlice(payload, 200)}`;\n }\n continue;\n }\n\n const eventType = parsed.event_type as string | undefined;\n if (!eventType) continue;\n\n if (eventType === \"content.delta\") {\n const delta = parsed.delta as Record<string, unknown> | undefined;\n if (!delta) continue;\n\n if (delta.type === \"text\" && typeof delta.text === \"string\") {\n content += delta.text;\n } else if (delta.type === \"function_call\") {\n toolCalls.push({\n name: String(delta.name ?? \"\"),\n arguments:\n typeof delta.arguments === \"string\"\n ? delta.arguments\n : JSON.stringify(delta.arguments ?? {}),\n ...(delta.id ? { id: String(delta.id) } : {}),\n });\n } else if (delta.type === \"thought_summary\" && typeof delta.text === \"string\") {\n reasoning += delta.text;\n }\n }\n }\n\n if (toolCalls.length > 0) {\n return {\n ...(content ? { content } : {}),\n toolCalls,\n ...(reasoning ? { reasoning } : {}),\n ...(droppedChunks > 0 ? { droppedChunks } : {}),\n ...(firstDroppedSample ? { firstDroppedSample } : {}),\n };\n }\n\n return {\n content,\n ...(reasoning ? { reasoning } : {}),\n ...(droppedChunks > 0 ? { droppedChunks } : {}),\n ...(firstDroppedSample ? { firstDroppedSample } : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Dispatch helper — pick the right collapse function by provider\n// ---------------------------------------------------------------------------\n\n/**\n * Collapse a streaming response body into a non-streaming fixture response.\n * Returns null if the content type is not a known streaming format.\n * Falls back to OpenAI SSE parsing for unrecognized provider keys with text/event-stream.\n */\nexport function collapseStreamingResponse(\n contentType: string,\n providerKey: RecordProviderKey,\n body: string | Buffer,\n logger?: Logger,\n): CollapseResult | null {\n const ct = contentType.toLowerCase();\n\n if (ct.includes(\"application/vnd.amazon.eventstream\")) {\n const buf = typeof body === \"string\" ? Buffer.from(body, \"binary\") : body;\n return collapseBedrockEventStream(buf);\n }\n\n if (ct.includes(\"application/x-ndjson\")) {\n const str = typeof body === \"string\" ? body : body.toString(\"utf8\");\n return collapseOllamaNDJSON(str);\n }\n\n if (ct.includes(\"text/event-stream\")) {\n const str = typeof body === \"string\" ? body : body.toString(\"utf8\");\n switch (providerKey) {\n case \"openai\":\n case \"azure\":\n return collapseOpenAISSE(str);\n case \"anthropic\":\n return collapseAnthropicSSE(str);\n case \"gemini\":\n case \"vertexai\":\n return collapseGeminiSSE(str);\n case \"gemini-interactions\":\n return collapseGeminiInteractionsSSE(str);\n case \"cohere\":\n return collapseCohereSSE(str);\n case \"bedrock\":\n return collapseAnthropicSSE(str);\n default:\n logger?.warn(\n `[stream-collapse] unknown SSE provider \"${providerKey}\", falling back to OpenAI SSE format`,\n );\n return collapseOpenAISSE(str);\n }\n }\n\n return null;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"stream-collapse.js","names":[],"sources":["../src/stream-collapse.ts"],"sourcesContent":["/**\n * Stream collapsing functions for record-and-replay.\n *\n * Each function takes a raw streaming response body (SSE, NDJSON, or binary\n * EventStream) and collapses it into a non-streaming fixture response\n * containing `{ content }`, `{ toolCalls }`, or both when the stream includes\n * text followed by tool calls.\n */\n\nimport { crc32 } from \"node:zlib\";\nimport type { RecordProviderKey, ToolCall } from \"./types.js\";\nimport type { Logger } from \"./logger.js\";\nimport { isHarmonyContent, parseHarmonyContent } from \"./harmony.js\";\n\n// ---------------------------------------------------------------------------\n// Result type shared by all collapse functions\n// ---------------------------------------------------------------------------\n\nexport interface CollapseResult {\n content?: string;\n reasoning?: string;\n /**\n * The real cryptographic `signature` value captured from an Anthropic\n * `signature_delta`. Carried so a recorded real-provider thinking turn can\n * replay its ACTUAL signature instead of aimock's placeholder. Absent when the\n * stream carried no signature. Single-signature assumption: a turn with\n * MULTIPLE thinking blocks collapses to one merged `reasoning` string carrying\n * only the FINAL block's signature (last-signature-wins) — per-block fidelity\n * is not preserved. The recorder persists this only alongside a non-empty\n * `reasoning` (a bare signature has nothing to attach to on replay); see\n * `TextResponse.reasoningSignature` in types.ts.\n */\n reasoningSignature?: string;\n /**\n * The opaque `data` payload(s) of any Anthropic `redacted_thinking` blocks, in\n * stream order. Captured so a recorded redacted-thinking turn round-trips its\n * encrypted reasoning faithfully. Absent when none present.\n */\n redactedThinking?: string[];\n webSearches?: string[];\n toolCalls?: ToolCall[];\n droppedChunks?: number;\n firstDroppedSample?: string;\n truncated?: boolean;\n audioB64?: string;\n audioMimeType?: string;\n /**\n * Set when harmony channel tokens were present in the accumulated content but\n * could NOT be parsed into a complete, valid harmony structure. The content\n * is preserved VERBATIM, so this is NOT transport loss — it is distinct from\n * `droppedChunks` / `truncated`, which are reserved for genuine transport loss\n * (malformed SSE/NDJSON frames, CRC mismatch). The caller surfaces this as a\n * dedicated warning rather than a dropped/truncated-chunk warning.\n */\n harmonyUnparsed?: true;\n /** Short human-readable note accompanying {@link harmonyUnparsed}. */\n harmonyNote?: string;\n}\n\n/**\n * The opaque `data` of a non-empty Anthropic `redacted_thinking` block, or\n * `undefined` if `block` is not a redacted_thinking block or carries empty/no\n * data. NON-EMPTY is required: the replay-side validator rejects a leading\n * empty-data redacted_thinking block, so recording `data: \"\"` would yield a\n * fixture that 400s under strict replay. Shared by every capture site (SSE,\n * Anthropic-native binary, non-streaming recorder) so the rule stays in one\n * place.\n */\nexport function capturedRedactedData(\n block: Record<string, unknown> | undefined,\n): string | undefined {\n if (\n block?.type === \"redacted_thinking\" &&\n typeof block.data === \"string\" &&\n block.data.length > 0\n ) {\n return block.data;\n }\n return undefined;\n}\n\n/**\n * Slice the first `max` UTF-16 code units of `s` for a diagnostic sample,\n * trimming a trailing lone high-surrogate so the resulting sample never ends on\n * a lone high surrogate (i.e. never mid-surrogate-pair).\n */\nfunction surrogateSafeSlice(s: string, max: number): string {\n let out = s.slice(0, max);\n if (out.length > 0) {\n const last = out.charCodeAt(out.length - 1);\n // A high surrogate (U+D800..U+DBFF) at the end is the lead of a split pair.\n if (last >= 0xd800 && last <= 0xdbff) {\n out = out.slice(0, -1);\n }\n }\n return out;\n}\n\n/**\n * Split a raw SSE body into per-event blocks.\n *\n * Events are delimited by a blank line. Real HTTP/SSE transports use CRLF\n * (`\\r\\n`) line endings, so the inter-event delimiter is `\\r\\n\\r\\n` (which\n * contains no `\\n\\n` substring) and each line ends with a trailing `\\r`.\n * Splitting on `/\\r?\\n\\r?\\n/` handles LF, CRLF, and mixed streams; per-line\n * `\\r` trimming happens in {@link splitSSELines}. Blank blocks are dropped.\n */\nfunction splitSSEEvents(body: string): string[] {\n return body.split(/\\r?\\n\\r?\\n/).filter((block) => block.trim().length > 0);\n}\n\n/**\n * Split a single SSE event block into its lines, trimming a trailing `\\r` so\n * CRLF streams parse identically to LF streams.\n */\nfunction splitSSELines(block: string): string[] {\n return block.split(\"\\n\").map((line) => (line.endsWith(\"\\r\") ? line.slice(0, -1) : line));\n}\n\n/**\n * Extract the SSE `data` field from a single event block's lines.\n *\n * Per the SSE spec a single event may carry MULTIPLE `data:` lines; the field\n * value is every data line's content joined with \"\\n\". Collecting only the\n * first `data:` line (e.g. via `.find`) corrupts payloads that a server split\n * across lines. Callers MUST pass lines produced by {@link splitSSELines} so\n * any trailing `\\r` is already stripped. Returns the joined payload (with the\n * leading \"data:\" prefix and one optional leading space stripped per line), or\n * `undefined` when the block contains no `data:` line.\n */\nfunction extractSSEData(lines: string[]): string | undefined {\n const dataParts: string[] = [];\n for (const line of lines) {\n if (!line.startsWith(\"data:\")) continue;\n // Strip \"data:\" then a single optional leading space, per the SSE spec.\n let part = line.slice(5);\n if (part.startsWith(\" \")) part = part.slice(1);\n dataParts.push(part);\n }\n if (dataParts.length === 0) return undefined;\n return dataParts.join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// 1. OpenAI SSE\n// ---------------------------------------------------------------------------\n\n/**\n * Collapse OpenAI Chat Completions SSE stream into a single response.\n *\n * Format:\n * data: {\"id\":\"chatcmpl-123\",\"choices\":[{\"delta\":{\"content\":\"Hello\"}}]}\\n\\n\n * data: [DONE]\\n\\n\n */\nexport function collapseOpenAISSE(body: string): CollapseResult {\n const lines = splitSSEEvents(body);\n let content = \"\";\n let reasoning = \"\";\n const webSearchQueries: string[] = [];\n let droppedChunks = 0;\n let firstDroppedSample: string | undefined;\n let harmonyUnparsed = false;\n let harmonyNote: string | undefined;\n const toolCallMap = new Map<number, { id: string; name: string; arguments: string }>();\n // Fallback keying for deltas that OMIT `index`. Without this, every\n // index-less delta collapses under one `undefined`/NaN key, merging distinct\n // tool calls and corrupting arguments. Index-less fragments that share an\n // `id` correlate via `idKeyMap`; otherwise each gets a fresh synthetic key\n // assigned from a counter kept above any real index so sort order is stable.\n // The 1_000_000 sentinel assumes real provider tool-call indices stay below\n // it (they are small per-stream counters), so synthetic keys never collide.\n let nextSyntheticIndex = 1_000_000;\n const idKeyMap = new Map<string, number>();\n\n for (const line of lines) {\n const data = extractSSEData(splitSSELines(line));\n if (data === undefined) continue;\n\n const payload = data.trim();\n if (payload === \"[DONE]\") continue;\n\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(payload) as Record<string, unknown>;\n } catch (err) {\n droppedChunks++;\n if (droppedChunks === 1) {\n const msg = err instanceof Error ? err.message : \"unknown\";\n firstDroppedSample = `parse failed (${msg}): ${surrogateSafeSlice(payload, 200)}`;\n }\n continue;\n }\n\n // Responses API reasoning events\n if (\n parsed.type === \"response.reasoning_summary_text.delta\" &&\n typeof parsed.delta === \"string\"\n ) {\n reasoning += parsed.delta;\n continue;\n }\n\n // Responses API web search events\n if (parsed.type === \"response.output_item.done\") {\n const item = parsed.item as Record<string, unknown> | undefined;\n if (item?.type === \"web_search_call\") {\n const action = item.action as Record<string, unknown> | undefined;\n if (action && typeof action.query === \"string\") {\n webSearchQueries.push(action.query);\n continue;\n }\n }\n }\n\n // Responses API text content events\n if (parsed.type === \"response.output_text.delta\" && typeof parsed.delta === \"string\") {\n content += parsed.delta;\n continue;\n }\n\n // Skip other Responses API structural events\n if (typeof parsed.type === \"string\" && parsed.type.startsWith(\"response.\")) {\n continue;\n }\n\n const choices = parsed.choices as Array<Record<string, unknown>> | undefined;\n if (!choices || choices.length === 0) continue;\n\n const delta = choices[0].delta as Record<string, unknown> | undefined;\n if (!delta) continue;\n\n // Reasoning content (OpenRouter / chat completions format)\n if (typeof delta.reasoning_content === \"string\") {\n reasoning += delta.reasoning_content;\n }\n\n // Text content\n if (typeof delta.content === \"string\") {\n content += delta.content;\n }\n\n // Tool calls\n const toolCalls = delta.tool_calls as Array<Record<string, unknown>> | undefined;\n if (toolCalls) {\n for (const tc of toolCalls) {\n const fn = tc.function as Record<string, unknown> | undefined;\n const rawId = typeof tc.id === \"string\" ? tc.id : undefined;\n\n // Resolve a stable map key. Prefer the streamed `index`; when it is\n // absent, correlate by `id` if present, else mint a fresh synthetic\n // key so distinct index-less calls never merge.\n let index: number;\n if (typeof tc.index === \"number\") {\n index = tc.index;\n } else if (rawId !== undefined) {\n const existing = idKeyMap.get(rawId);\n if (existing !== undefined) {\n index = existing;\n } else {\n index = nextSyntheticIndex++;\n idKeyMap.set(rawId, index);\n }\n } else {\n index = nextSyntheticIndex++;\n }\n\n if (!toolCallMap.has(index)) {\n toolCallMap.set(index, {\n id: rawId ?? \"\",\n name: (fn?.name as string) ?? \"\",\n arguments: \"\",\n });\n }\n\n const entry = toolCallMap.get(index)!;\n if (fn?.name && typeof fn.name === \"string\" && !entry.name) {\n entry.name = fn.name;\n }\n if (tc.id && typeof tc.id === \"string\" && !entry.id) {\n entry.id = tc.id;\n }\n if (fn?.arguments && typeof fn.arguments === \"string\") {\n entry.arguments += fn.arguments;\n }\n }\n }\n }\n\n // Open-weight gpt-oss models (Ollama / vLLM / OpenRouter) stream tool calls\n // as raw harmony channel tokens inside delta.content rather than structured\n // delta.tool_calls. Harmony parsing is FALLBACK-ONLY: attempt it ONLY when\n // there are NO structured delta.tool_calls. If structured tool calls exist,\n // any harmony-looking content is prose — never merged (no phantom tool call),\n // never stamped as truncated/dropped. When harmony IS the only source, a\n // successful parse routes channels (content/reasoning/toolCalls); a failure\n // preserves content VERBATIM and surfaces the distinct `harmonyUnparsed`\n // signal (NOT droppedChunks/truncated — the bytes are not lost).\n const harmonyToolCalls: ToolCall[] = [];\n if (toolCallMap.size === 0 && isHarmonyContent(content)) {\n const parsed = parseHarmonyContent(content);\n if (parsed.failed) {\n harmonyUnparsed = true;\n harmonyNote = `harmony tokens present but unparseable; content preserved verbatim: ${surrogateSafeSlice(content, 200)}`;\n } else {\n content = parsed.content;\n if (parsed.reasoning) {\n reasoning += parsed.reasoning;\n }\n harmonyToolCalls.push(...parsed.toolCalls);\n }\n }\n\n if (toolCallMap.size > 0 || harmonyToolCalls.length > 0) {\n const sorted = Array.from(toolCallMap.entries()).sort(([a], [b]) => a - b);\n return {\n ...(content ? { content } : {}),\n // Fallback-only: harmonyToolCalls are populated ONLY in the\n // no-structured-calls branch, so this is never a merge of both sources.\n toolCalls: [\n ...sorted.map(([, tc]) => ({\n name: tc.name,\n arguments: tc.arguments,\n ...(tc.id ? { id: tc.id } : {}),\n })),\n ...harmonyToolCalls,\n ],\n // Reasoning is preserved alongside tool calls for ALL structured streams\n // (DeepSeek/OpenRouter reasoning_content, harmony analysis channel), at\n // parity with every other collapser and the non-streaming path.\n ...(reasoning ? { reasoning } : {}),\n // webSearches parity with the text-only return branch.\n ...(webSearchQueries.length > 0 ? { webSearches: webSearchQueries } : {}),\n ...(droppedChunks > 0 ? { droppedChunks } : {}),\n ...(firstDroppedSample ? { firstDroppedSample } : {}),\n ...(harmonyUnparsed ? { harmonyUnparsed: true } : {}),\n ...(harmonyNote ? { harmonyNote } : {}),\n };\n }\n\n return {\n content,\n ...(reasoning ? { reasoning } : {}),\n ...(webSearchQueries.length > 0 ? { webSearches: webSearchQueries } : {}),\n ...(droppedChunks > 0 ? { droppedChunks } : {}),\n ...(firstDroppedSample ? { firstDroppedSample } : {}),\n ...(harmonyUnparsed ? { harmonyUnparsed: true } : {}),\n ...(harmonyNote ? { harmonyNote } : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// 2. Anthropic SSE\n// ---------------------------------------------------------------------------\n\n/**\n * Collapse Anthropic Claude Messages SSE stream into a single response.\n *\n * Format:\n * event: message_start\\ndata: {...}\\n\\n\n * event: content_block_delta\\ndata: {\"delta\":{\"type\":\"text_delta\",\"text\":\"Hello\"}}\\n\\n\n */\nexport function collapseAnthropicSSE(body: string): CollapseResult {\n const blocks = splitSSEEvents(body);\n let content = \"\";\n let reasoning = \"\";\n // Real cryptographic signature captured from a `signature_delta`; stays\n // undefined when the stream carried none (e.g. aimock's own placeholder turns\n // or non-thinking turns). Carried so a recorded real-provider thinking turn\n // can replay its ACTUAL signature instead of aimock's placeholder.\n let reasoningSignature: string | undefined;\n // Opaque `data` payloads of any `redacted_thinking` content blocks, in stream\n // order. Stays empty when none present. Carried so a recorded redacted-thinking\n // turn round-trips its encrypted reasoning faithfully on replay.\n const redactedThinking: string[] = [];\n let droppedChunks = 0;\n let firstDroppedSample: string | undefined;\n const toolCallMap = new Map<number, { id: string; name: string; arguments: string }>();\n // Fallback keying for content blocks that OMIT `index` (mirrors the OpenAI /\n // Cohere / Bedrock guards). Without it, every index-less block collapses\n // under one `undefined` key, merging distinct tool_use blocks. Index-less\n // starts mint a fresh synthetic key (kept above any real index so sort order\n // is stable). Despite its name, `lastSyntheticIndex` tracks whichever\n // tool_use start most recently opened REGARDLESS of whether its index was\n // real or synthetic (it is set on every tool_use start; thinking /\n // redacted_thinking starts do not touch it), so an index-less delta\n // correlates to the most-recent tool_use start — not just to the last\n // synthetic one. The 1_000_000 sentinel assumes real provider indices stay\n // below it.\n let nextSyntheticIndex = 1_000_000;\n let lastSyntheticIndex: number | undefined;\n\n for (const block of blocks) {\n const lines = splitSSELines(block);\n const eventLine = lines.find((l) => l.startsWith(\"event:\"));\n const data = extractSSEData(lines);\n if (data === undefined) continue;\n\n const eventType = eventLine ? eventLine.slice(6).trim() : \"\";\n const payload = data.trim();\n\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(payload) as Record<string, unknown>;\n } catch (err) {\n droppedChunks++;\n if (droppedChunks === 1) {\n const msg = err instanceof Error ? err.message : \"unknown\";\n firstDroppedSample = `parse failed (${msg}): ${surrogateSafeSlice(payload, 200)}`;\n }\n continue;\n }\n\n if (eventType === \"content_block_start\") {\n const contentBlock = parsed.content_block as Record<string, unknown> | undefined;\n // A `redacted_thinking` block carries its encrypted reasoning in an opaque\n // `data` string on the start event (no deltas follow). Capture it so the\n // recorded turn can replay the redacted block faithfully\n // (see capturedRedactedData for the non-empty rule).\n const redactedData = capturedRedactedData(contentBlock);\n if (redactedData !== undefined) {\n redactedThinking.push(redactedData);\n }\n if (contentBlock?.type === \"tool_use\") {\n // Prefer the streamed `index`; when absent, mint a fresh synthetic key\n // so distinct index-less tool_use blocks never merge.\n let index: number;\n if (typeof parsed.index === \"number\") {\n index = parsed.index;\n } else {\n index = nextSyntheticIndex++;\n }\n lastSyntheticIndex = index;\n toolCallMap.set(index, {\n id: (contentBlock.id as string) ?? \"\",\n name: (contentBlock.name as string) ?? \"\",\n arguments: \"\",\n });\n }\n }\n\n if (eventType === \"content_block_delta\") {\n const delta = parsed.delta as Record<string, unknown> | undefined;\n if (!delta) continue;\n\n if (delta.type === \"text_delta\" && typeof delta.text === \"string\") {\n content += delta.text;\n }\n\n if (delta.type === \"thinking_delta\" && typeof delta.thinking === \"string\") {\n reasoning += delta.thinking;\n }\n\n // The real cryptographic signature arrives via a trailing\n // `signature_delta` (the `content_block_start` carried \"\"). Capture the\n // last one seen so a recorded thinking turn replays its actual signature.\n // Last-signature-wins: a turn with MULTIPLE thinking blocks overwrites this\n // on each block, so the merged `reasoning` string ends up bound only to the\n // FINAL block's signature — per-block signatures are not preserved.\n if (delta.type === \"signature_delta\" && typeof delta.signature === \"string\") {\n reasoningSignature = delta.signature;\n }\n\n if (delta.type === \"input_json_delta\" && typeof delta.partial_json === \"string\") {\n // Use the streamed `index` when present; otherwise correlate to the\n // most recent tool_use start (mirrors the start-side fallback).\n const index = typeof parsed.index === \"number\" ? parsed.index : lastSyntheticIndex;\n // A delta that cannot correlate to any known start (no streamed index\n // AND no prior start, or a stale index with no entry) would otherwise\n // silently lose its args. Account for it as a dropped chunk instead of\n // vanishing (mirrors the Cohere uncorrelated-delta path).\n const entry = index !== undefined ? toolCallMap.get(index) : undefined;\n if (entry) {\n entry.arguments += delta.partial_json;\n } else {\n droppedChunks++;\n if (droppedChunks === 1) {\n firstDroppedSample = `input_json_delta with no correlating tool_use start: ${surrogateSafeSlice(\n payload,\n 200,\n )}`;\n }\n }\n }\n }\n }\n\n if (toolCallMap.size > 0) {\n const sorted = Array.from(toolCallMap.entries()).sort(([a], [b]) => a - b);\n return {\n ...(content ? { content } : {}),\n toolCalls: sorted.map(([, tc]) => ({\n name: tc.name,\n arguments: tc.arguments,\n ...(tc.id ? { id: tc.id } : {}),\n })),\n ...(reasoning ? { reasoning } : {}),\n ...(reasoningSignature ? { reasoningSignature } : {}),\n ...(redactedThinking.length > 0 ? { redactedThinking } : {}),\n ...(droppedChunks > 0 ? { droppedChunks } : {}),\n ...(firstDroppedSample ? { firstDroppedSample } : {}),\n };\n }\n\n return {\n content,\n ...(reasoning ? { reasoning } : {}),\n ...(reasoningSignature ? { reasoningSignature } : {}),\n ...(redactedThinking.length > 0 ? { redactedThinking } : {}),\n ...(droppedChunks > 0 ? { droppedChunks } : {}),\n ...(firstDroppedSample ? { firstDroppedSample } : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// 3. Gemini SSE\n// ---------------------------------------------------------------------------\n\n/**\n * Collapse Gemini SSE stream into a single response.\n *\n * Format (data-only, no event prefix, no [DONE]):\n * data: {\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"Hello\"}]}}]}\\n\\n\n */\nexport function collapseGeminiSSE(body: string): CollapseResult {\n const lines = splitSSEEvents(body);\n let content = \"\";\n let reasoning = \"\";\n let droppedChunks = 0;\n let firstDroppedSample: string | undefined;\n let audioB64 = \"\";\n let audioMimeType: string | undefined;\n const toolCalls: ToolCall[] = [];\n\n for (const line of lines) {\n const data = extractSSEData(splitSSELines(line));\n if (data === undefined) continue;\n\n const payload = data.trim();\n\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(payload) as Record<string, unknown>;\n } catch (err) {\n droppedChunks++;\n if (droppedChunks === 1) {\n const msg = err instanceof Error ? err.message : \"unknown\";\n firstDroppedSample = `parse failed (${msg}): ${surrogateSafeSlice(payload, 200)}`;\n }\n continue;\n }\n\n const candidates = parsed.candidates as Array<Record<string, unknown>> | undefined;\n if (!candidates || candidates.length === 0) continue;\n\n const candidateContent = candidates[0].content as Record<string, unknown> | undefined;\n if (!candidateContent) continue;\n\n const parts = candidateContent.parts as Array<Record<string, unknown>> | undefined;\n if (!parts || parts.length === 0) continue;\n\n for (const part of parts) {\n if (part.functionCall) {\n const fc = part.functionCall as Record<string, unknown>;\n toolCalls.push({\n name: String(fc.name ?? \"\"),\n // Default undefined/object args to a JSON object string (matches\n // collapseGeminiInteractionsSSE / Ollama). JSON.stringify(undefined)\n // would otherwise yield the VALUE undefined, violating the\n // ToolCall.arguments:string contract.\n arguments:\n typeof fc.args === \"string\" ? (fc.args as string) : JSON.stringify(fc.args ?? {}),\n });\n } else if (\n part.inlineData &&\n typeof (part.inlineData as Record<string, unknown>).mimeType === \"string\" &&\n ((part.inlineData as Record<string, unknown>).mimeType as string).startsWith(\"audio/\")\n ) {\n const inlineData = part.inlineData as Record<string, unknown>;\n if (!audioMimeType) {\n audioMimeType = inlineData.mimeType as string;\n }\n if (typeof inlineData.data === \"string\") {\n audioB64 += inlineData.data;\n }\n } else if (typeof part.text === \"string\") {\n if (part.thought) {\n reasoning += part.text;\n } else {\n content += part.text;\n }\n }\n }\n }\n\n if (audioB64) {\n // Preserve any content / reasoning / tool calls accumulated in the same\n // stream — a Gemini turn can interleave audio with text and functionCall\n // parts, and the early return must not silently drop them.\n return {\n audioB64,\n audioMimeType,\n ...(content ? { content } : {}),\n ...(reasoning ? { reasoning } : {}),\n ...(toolCalls.length > 0 ? { toolCalls } : {}),\n ...(droppedChunks > 0 ? { droppedChunks } : {}),\n ...(firstDroppedSample ? { firstDroppedSample } : {}),\n };\n }\n\n if (toolCalls.length > 0) {\n return {\n ...(content ? { content } : {}),\n toolCalls,\n ...(reasoning ? { reasoning } : {}),\n ...(droppedChunks > 0 ? { droppedChunks } : {}),\n ...(firstDroppedSample ? { firstDroppedSample } : {}),\n };\n }\n\n return {\n content,\n ...(reasoning ? { reasoning } : {}),\n ...(droppedChunks > 0 ? { droppedChunks } : {}),\n ...(firstDroppedSample ? { firstDroppedSample } : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// 4. Ollama NDJSON\n// ---------------------------------------------------------------------------\n\n/**\n * Collapse Ollama NDJSON stream into a single response.\n *\n * /api/chat format:\n * {\"model\":\"llama3\",\"message\":{\"role\":\"assistant\",\"content\":\"Hello\"},\"done\":false}\\n\n *\n * /api/generate format:\n * {\"model\":\"llama3\",\"response\":\"Hello\",\"done\":false}\\n\n *\n * Open-weight gpt-oss served via Ollama streams harmony channel tokens inside\n * `message.content` (just like the OpenAI SSE path), so after accumulation the\n * content is run through the same fail-safe {@link parseHarmonyContent} gate to\n * capture structured tool calls / reasoning instead of leaking raw tokens.\n */\nexport function collapseOllamaNDJSON(body: string): CollapseResult {\n const lines = body.split(\"\\n\").filter((l) => l.trim().length > 0);\n let content = \"\";\n let reasoning = \"\";\n let droppedChunks = 0;\n let firstDroppedSample: string | undefined;\n let harmonyUnparsed = false;\n let harmonyNote: string | undefined;\n const toolCalls: ToolCall[] = [];\n\n for (const line of lines) {\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(line.trim()) as Record<string, unknown>;\n } catch (err) {\n droppedChunks++;\n if (droppedChunks === 1) {\n const msg = err instanceof Error ? err.message : \"unknown\";\n firstDroppedSample = `parse failed (${msg}): ${surrogateSafeSlice(line.trim(), 200)}`;\n }\n continue;\n }\n\n // /api/chat format\n const message = parsed.message as Record<string, unknown> | undefined;\n if (message) {\n if (typeof message.content === \"string\") {\n content += message.content;\n }\n\n // Tool calls\n if (Array.isArray(message.tool_calls)) {\n for (const tc of message.tool_calls as Array<Record<string, unknown>>) {\n const fn = tc.function as Record<string, unknown> | undefined;\n if (fn) {\n toolCalls.push({\n name: String(fn.name ?? \"\"),\n // Default undefined/object args to a JSON object (matching\n // collapseGeminiInteractionsSSE) — JSON.stringify(undefined)\n // would otherwise yield the literal string \"undefined\".\n arguments:\n typeof fn.arguments === \"string\"\n ? fn.arguments\n : JSON.stringify(fn.arguments ?? {}),\n });\n }\n }\n }\n }\n\n // /api/generate format\n else if (typeof parsed.response === \"string\") {\n content += parsed.response;\n }\n }\n\n // Open-weight gpt-oss served via Ollama streams harmony channel tokens inside\n // message.content (same as the OpenAI SSE path). Harmony parsing is\n // FALLBACK-ONLY: attempt it ONLY when there are NO structured message\n // tool_calls. If structured tool calls exist, harmony-looking content is\n // prose — never merged (no phantom), never stamped truncated/dropped. On a\n // harmony failure the content is preserved VERBATIM and surfaced via the\n // distinct `harmonyUnparsed` signal (NOT droppedChunks/truncated).\n if (toolCalls.length === 0 && isHarmonyContent(content)) {\n const parsedHarmony = parseHarmonyContent(content);\n if (parsedHarmony.failed) {\n harmonyUnparsed = true;\n harmonyNote = `harmony tokens present but unparseable; content preserved verbatim: ${surrogateSafeSlice(content, 200)}`;\n } else {\n content = parsedHarmony.content;\n if (parsedHarmony.reasoning) {\n reasoning += parsedHarmony.reasoning;\n }\n toolCalls.push(...parsedHarmony.toolCalls);\n }\n }\n\n if (toolCalls.length > 0) {\n return {\n ...(content ? { content } : {}),\n toolCalls,\n ...(reasoning ? { reasoning } : {}),\n ...(droppedChunks > 0 ? { droppedChunks } : {}),\n ...(firstDroppedSample ? { firstDroppedSample } : {}),\n ...(harmonyUnparsed ? { harmonyUnparsed: true } : {}),\n ...(harmonyNote ? { harmonyNote } : {}),\n };\n }\n\n return {\n content,\n ...(reasoning ? { reasoning } : {}),\n ...(droppedChunks > 0 ? { droppedChunks } : {}),\n ...(firstDroppedSample ? { firstDroppedSample } : {}),\n ...(harmonyUnparsed ? { harmonyUnparsed: true } : {}),\n ...(harmonyNote ? { harmonyNote } : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// 5. Cohere SSE\n// ---------------------------------------------------------------------------\n\n/**\n * Collapse Cohere SSE stream into a single response.\n *\n * Format:\n * event: content-delta\\ndata: {\"type\":\"content-delta\",\"delta\":{\"message\":{\"content\":{\"text\":\"Hello\"}}}}\\n\\n\n */\nexport function collapseCohereSSE(body: string): CollapseResult {\n const blocks = splitSSEEvents(body);\n let content = \"\";\n // Reasoning text assembled from `thinking` content-delta blocks. Cohere's\n // reasoning models stream a `content.type === \"thinking\"` block carrying a\n // `thinking` string before the `text` block; capture it so a recorded\n // reasoning turn round-trips its reasoning instead of dropping it.\n let reasoning = \"\";\n let droppedChunks = 0;\n let firstDroppedSample: string | undefined;\n const toolCallMap = new Map<number, { id: string; name: string; arguments: string }>();\n // Fallback keying for tool-call events that OMIT `index` (mirrors the\n // OpenAI guard). Without it, every index-less tool-call-start collapses\n // under one `undefined`/NaN key, merging distinct calls. Index-less starts\n // mint a fresh synthetic key. `lastStartKey` tracks the most-recent\n // tool-call-start key REGARDLESS of whether it was real or synthetic, so an\n // index-less tool-call-delta correlates to whichever start most recently\n // opened — not just to the last synthetic one. The 1_000_000 sentinel\n // assumes real provider indices stay below it.\n let nextSyntheticIndex = 1_000_000;\n let lastStartKey: number | undefined;\n\n for (const block of blocks) {\n const lines = splitSSELines(block);\n const eventLine = lines.find((l) => l.startsWith(\"event:\"));\n const data = extractSSEData(lines);\n if (data === undefined) continue;\n\n const eventType = eventLine ? eventLine.slice(6).trim() : \"\";\n const payload = data.trim();\n\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(payload) as Record<string, unknown>;\n } catch (err) {\n droppedChunks++;\n if (droppedChunks === 1) {\n const msg = err instanceof Error ? err.message : \"unknown\";\n firstDroppedSample = `parse failed (${msg}): ${surrogateSafeSlice(payload, 200)}`;\n }\n continue;\n }\n\n if (eventType === \"content-delta\") {\n const delta = parsed.delta as Record<string, unknown> | undefined;\n const message = delta?.message as Record<string, unknown> | undefined;\n const contentObj = message?.content as Record<string, unknown> | undefined;\n if (contentObj && contentObj.type === \"thinking\" && typeof contentObj.thinking === \"string\") {\n reasoning += contentObj.thinking;\n } else if (contentObj && typeof contentObj.text === \"string\") {\n content += contentObj.text;\n }\n }\n\n if (eventType === \"tool-call-start\") {\n let index: number;\n if (typeof parsed.index === \"number\") {\n index = parsed.index;\n } else {\n index = nextSyntheticIndex++;\n }\n // Track the most-recent start key (real OR synthetic) so a following\n // index-less delta correlates to whichever call just opened.\n lastStartKey = index;\n const delta = parsed.delta as Record<string, unknown> | undefined;\n const message = delta?.message as Record<string, unknown> | undefined;\n const toolCalls = message?.tool_calls as Record<string, unknown> | undefined;\n if (toolCalls) {\n const fn = toolCalls.function as Record<string, unknown> | undefined;\n toolCallMap.set(index, {\n id: (toolCalls.id as string) ?? \"\",\n name: (fn?.name as string) ?? \"\",\n arguments: \"\",\n });\n }\n }\n\n if (eventType === \"tool-call-delta\") {\n // Use the streamed `index` when present; otherwise correlate to the most\n // recent tool-call-start (real or synthetic key).\n const index = typeof parsed.index === \"number\" ? parsed.index : lastStartKey;\n const delta = parsed.delta as Record<string, unknown> | undefined;\n const message = delta?.message as Record<string, unknown> | undefined;\n const toolCalls = message?.tool_calls as Record<string, unknown> | undefined;\n if (toolCalls) {\n const fn = toolCalls.function as Record<string, unknown> | undefined;\n if (fn && typeof fn.arguments === \"string\") {\n // A delta that cannot correlate to any known start (no streamed\n // index AND no prior start) would otherwise silently lose its args.\n // Account for it as a dropped chunk instead of vanishing.\n const entry = index !== undefined ? toolCallMap.get(index) : undefined;\n if (entry) {\n entry.arguments += fn.arguments;\n } else {\n droppedChunks++;\n if (droppedChunks === 1) {\n firstDroppedSample = `tool-call-delta with no correlating start: ${surrogateSafeSlice(\n payload,\n 200,\n )}`;\n }\n }\n }\n }\n }\n }\n\n if (toolCallMap.size > 0) {\n const sorted = Array.from(toolCallMap.entries()).sort(([a], [b]) => a - b);\n return {\n ...(content ? { content } : {}),\n toolCalls: sorted.map(([, tc]) => ({\n name: tc.name,\n arguments: tc.arguments,\n ...(tc.id ? { id: tc.id } : {}),\n })),\n ...(reasoning ? { reasoning } : {}),\n ...(droppedChunks > 0 ? { droppedChunks } : {}),\n ...(firstDroppedSample ? { firstDroppedSample } : {}),\n };\n }\n\n return {\n content,\n ...(reasoning ? { reasoning } : {}),\n ...(droppedChunks > 0 ? { droppedChunks } : {}),\n ...(firstDroppedSample ? { firstDroppedSample } : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// 6. Bedrock EventStream (binary)\n// ---------------------------------------------------------------------------\n\n/**\n * Decode AWS Event Stream binary frames and extract JSON payloads.\n *\n * Binary frame layout:\n * [total_length: 4B uint32-BE]\n * [headers_length: 4B uint32-BE]\n * [prelude_crc32: 4B]\n * [headers: variable]\n * [payload: variable]\n * [message_crc32: 4B]\n */\nfunction decodeEventStreamFrames(buf: Buffer): {\n frames: Array<{ headers: Record<string, string>; payload: Buffer }>;\n truncated: boolean;\n} {\n const frames: Array<{ headers: Record<string, string>; payload: Buffer }> = [];\n let offset = 0;\n\n while (offset < buf.length) {\n if (offset + 12 > buf.length) break;\n\n const totalLength = buf.readUInt32BE(offset);\n const headersLength = buf.readUInt32BE(offset + 4);\n\n // Validate bounds: ensure the full frame is within the buffer\n if (totalLength < 12 || offset + totalLength > buf.length) {\n return { frames, truncated: true };\n }\n\n // Validate prelude CRC\n const preludeCrc = buf.readUInt32BE(offset + 8);\n const computedPreludeCrc = crc32(buf.subarray(offset, offset + 8));\n if (preludeCrc >>> 0 !== computedPreludeCrc >>> 0) {\n return { frames, truncated: true }; // Prelude CRC mismatch — stop parsing\n }\n\n // Parse headers\n const headersStart = offset + 12;\n const headersEnd = headersStart + headersLength;\n const payloadEnd = offset + totalLength - 4; // minus message CRC\n\n // Validate the headers region fits inside the frame. A frame can carry a\n // valid prelude CRC yet declare a `headersLength` that overruns the payload\n // region (the prelude CRC only covers total/headers length, not the body).\n // Without this guard a per-header read walks off the buffer and throws an\n // uncaught RangeError; treat it as truncation instead.\n if (headersEnd > payloadEnd || headersEnd > buf.length) {\n return { frames, truncated: true };\n }\n\n const headers: Record<string, string> = {};\n let hOffset = headersStart;\n let headerOverrun = false;\n\n while (hOffset < headersEnd) {\n // Each read must stay within the declared headers region. Bail out\n // (truncated) on any overrun rather than reading past the boundary.\n if (hOffset + 1 > headersEnd) {\n headerOverrun = true;\n break;\n }\n const nameLen = buf.readUInt8(hOffset);\n hOffset += 1;\n if (hOffset + nameLen + 1 + 2 > headersEnd) {\n headerOverrun = true;\n break;\n }\n const name = buf.subarray(hOffset, hOffset + nameLen).toString(\"utf8\");\n hOffset += nameLen;\n // Skip header type byte (type 7 = STRING)\n hOffset += 1;\n const valueLen = buf.readUInt16BE(hOffset);\n hOffset += 2;\n if (hOffset + valueLen > headersEnd) {\n headerOverrun = true;\n break;\n }\n const value = buf.subarray(hOffset, hOffset + valueLen).toString(\"utf8\");\n hOffset += valueLen;\n headers[name] = value;\n }\n\n if (headerOverrun) {\n return { frames, truncated: true };\n }\n\n // Extract payload\n const payloadStart = headersEnd;\n const payload = buf.subarray(payloadStart, payloadEnd);\n\n // Validate message CRC (covers entire frame minus last 4 bytes)\n const messageCrc = buf.readUInt32BE(offset + totalLength - 4);\n const computedMessageCrc = crc32(buf.subarray(offset, offset + totalLength - 4));\n if (messageCrc >>> 0 !== computedMessageCrc >>> 0) {\n return { frames, truncated: true }; // Message CRC mismatch — stop parsing\n }\n\n frames.push({ headers, payload });\n offset += totalLength;\n }\n\n return { frames, truncated: false };\n}\n\n/**\n * Collapse Bedrock binary Event Stream into a single response.\n *\n * Each frame contains a JSON payload with event types like:\n * contentBlockDelta, contentBlockStart, etc.\n */\nexport function collapseBedrockEventStream(body: Buffer): CollapseResult {\n const { frames, truncated } = decodeEventStreamFrames(body);\n let content = \"\";\n // Reasoning text assembled from Converse `reasoningContent.text` deltas. The\n // Bedrock Converse stream interleaves a `delta.reasoningContent` block carrying\n // the model's reasoning; capture it so a recorded reasoning turn round-trips\n // its reasoning instead of dropping it.\n let reasoning = \"\";\n // Anthropic-native extended-thinking fields (invoke-with-response-stream).\n // The native binary branch carries the same thinking/signature/redacted\n // channel as the Anthropic SSE collapser; it mirrors that path's\n // thinking/signature/redacted capture rules so a recorded reasoning turn\n // round-trips instead of silently dropping it. Unlike the SSE path, binary\n // tool_use correlation has no index-less fallback and relies on an explicit\n // `index`.\n let reasoningSignature: string | undefined;\n const redactedThinking: string[] = [];\n let droppedChunks = 0;\n let firstDroppedSample: string | undefined;\n const toolCallMap = new Map<number, { id: string; name: string; arguments: string }>();\n\n for (const frame of frames) {\n const frameStr = frame.payload.toString(\"utf8\");\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(frameStr) as Record<string, unknown>;\n } catch (err) {\n droppedChunks++;\n if (droppedChunks === 1) {\n const msg = err instanceof Error ? err.message : \"unknown\";\n firstDroppedSample = `parse failed (${msg}): ${surrogateSafeSlice(frameStr, 200)}`;\n }\n continue;\n }\n\n // Anthropic Messages format (invoke-with-response-stream): flat payload with \"type\" field\n if (parsed.type === \"content_block_delta\") {\n const delta = parsed.delta as Record<string, unknown> | undefined;\n if (delta?.type === \"text_delta\" && typeof delta.text === \"string\") {\n content += delta.text;\n }\n if (delta?.type === \"thinking_delta\" && typeof delta.thinking === \"string\") {\n reasoning += delta.thinking;\n }\n // Last-signature-wins: a turn with MULTIPLE thinking blocks overwrites\n // this on each block, so the merged `reasoning` ends up bound only to the\n // FINAL block's signature (mirrors collapseAnthropicSSE).\n if (delta?.type === \"signature_delta\" && typeof delta.signature === \"string\") {\n reasoningSignature = delta.signature;\n }\n if (delta?.type === \"input_json_delta\" && typeof delta.partial_json === \"string\") {\n const index = parsed.index as number | undefined;\n // An arg delta that cannot correlate to a known tool_use start would\n // otherwise silently lose its args. Account for it as a dropped chunk\n // instead of vanishing (mirrors the Cohere uncorrelated-delta path).\n const entry = index !== undefined ? toolCallMap.get(index) : undefined;\n if (entry) {\n entry.arguments += delta.partial_json;\n } else {\n droppedChunks++;\n if (droppedChunks === 1) {\n firstDroppedSample = `input_json_delta with no correlating tool_use start: ${surrogateSafeSlice(\n frameStr,\n 200,\n )}`;\n }\n }\n }\n continue;\n }\n if (parsed.type === \"content_block_start\") {\n const block = parsed.content_block as Record<string, unknown> | undefined;\n const index = parsed.index as number | undefined;\n // A `redacted_thinking` block carries its encrypted reasoning in an\n // opaque `data` string on the start event (no deltas follow). Capture it\n // so the recorded turn replays the redacted block faithfully\n // (mirrors collapseAnthropicSSE; see capturedRedactedData).\n const redactedData = capturedRedactedData(block);\n if (redactedData !== undefined) {\n redactedThinking.push(redactedData);\n }\n if (block?.type === \"tool_use\" && index !== undefined) {\n toolCallMap.set(index, {\n id: (block.id as string) ?? \"\",\n name: (block.name as string) ?? \"\",\n arguments: \"\",\n });\n }\n continue;\n }\n\n // Converse format (converse-stream): camelCase wrapper keys\n // contentBlockStart — may initiate a tool_use block\n if (parsed.contentBlockStart) {\n const blockStart = parsed.contentBlockStart as Record<string, unknown>;\n const index = (parsed.contentBlockIndex ?? blockStart.contentBlockIndex) as\n | number\n | undefined;\n const start = blockStart.start as Record<string, unknown> | undefined;\n if (start?.toolUse && index !== undefined) {\n const toolUse = start.toolUse as Record<string, unknown>;\n toolCallMap.set(index, {\n id: (toolUse.toolUseId as string) ?? \"\",\n name: (toolUse.name as string) ?? \"\",\n arguments: \"\",\n });\n }\n }\n\n // contentBlockDelta\n if (parsed.contentBlockDelta) {\n const blockDelta = parsed.contentBlockDelta as Record<string, unknown>;\n const index = (parsed.contentBlockIndex ?? blockDelta.contentBlockIndex) as\n | number\n | undefined;\n const delta = blockDelta.delta as Record<string, unknown> | undefined;\n if (!delta) continue;\n\n // Text delta\n if (typeof delta.text === \"string\") {\n content += delta.text;\n }\n\n // Reasoning delta — Converse carries reasoning in `reasoningContent.text`.\n // The Converse branch intentionally captures no signature/redacted channel:\n // Converse has no `signature_delta`/`redacted_thinking` wire shape, so the\n // asymmetry with the Anthropic-native branch above is by design, not a gap\n // to be \"fixed\" later.\n if (typeof delta.reasoningContent === \"object\" && delta.reasoningContent !== null) {\n const reasoningDelta = delta.reasoningContent as Record<string, unknown>;\n if (typeof reasoningDelta.text === \"string\") {\n reasoning += reasoningDelta.text;\n }\n }\n\n // Tool use input JSON delta\n if (typeof delta.toolUse === \"object\" && delta.toolUse !== null) {\n const toolUseDelta = delta.toolUse as Record<string, unknown>;\n if (typeof toolUseDelta.input === \"string\") {\n // An arg delta that cannot correlate to a known tool_use start would\n // otherwise silently lose its args. Account for it as a dropped chunk\n // instead of vanishing (mirrors the Cohere uncorrelated-delta path).\n const entry = index !== undefined ? toolCallMap.get(index) : undefined;\n if (entry) {\n entry.arguments += toolUseDelta.input;\n } else {\n droppedChunks++;\n if (droppedChunks === 1) {\n firstDroppedSample = `toolUse.input delta with no correlating tool_use start: ${surrogateSafeSlice(\n frameStr,\n 200,\n )}`;\n }\n }\n }\n }\n }\n }\n\n if (toolCallMap.size > 0) {\n const sorted = Array.from(toolCallMap.entries()).sort(([a], [b]) => a - b);\n return {\n toolCalls: sorted.map(([, tc]) => ({\n name: tc.name,\n arguments: tc.arguments,\n ...(tc.id ? { id: tc.id } : {}),\n })),\n ...(reasoning ? { reasoning } : {}),\n ...(reasoningSignature ? { reasoningSignature } : {}),\n ...(redactedThinking.length > 0 ? { redactedThinking } : {}),\n ...(droppedChunks > 0 ? { droppedChunks } : {}),\n ...(firstDroppedSample ? { firstDroppedSample } : {}),\n ...(truncated ? { truncated } : {}),\n };\n }\n\n return {\n content,\n ...(reasoning ? { reasoning } : {}),\n ...(reasoningSignature ? { reasoningSignature } : {}),\n ...(redactedThinking.length > 0 ? { redactedThinking } : {}),\n ...(droppedChunks > 0 ? { droppedChunks } : {}),\n ...(firstDroppedSample ? { firstDroppedSample } : {}),\n ...(truncated ? { truncated } : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// 7. Gemini Interactions SSE\n// ---------------------------------------------------------------------------\n\n/**\n * Collapse Gemini Interactions SSE stream into a single response.\n *\n * Format (data-only, event_type inside JSON):\n * data: {\"event_type\":\"content.delta\",\"index\":0,\"delta\":{\"type\":\"text\",\"text\":\"Hello\"}}\\n\\n\n * data: {\"event_type\":\"interaction.complete\",\"interaction\":{\"id\":\"...\",\"usage\":{...}}}\\n\\n\n */\nexport function collapseGeminiInteractionsSSE(body: string): CollapseResult {\n const lines = splitSSEEvents(body);\n let content = \"\";\n let reasoning = \"\";\n let droppedChunks = 0;\n let firstDroppedSample: string | undefined;\n const toolCalls: ToolCall[] = [];\n\n for (const line of lines) {\n const data = extractSSEData(splitSSELines(line));\n if (data === undefined) continue;\n\n const payload = data.trim();\n\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(payload) as Record<string, unknown>;\n } catch (err) {\n droppedChunks++;\n if (droppedChunks === 1) {\n const msg = err instanceof Error ? err.message : \"unknown\";\n firstDroppedSample = `parse failed (${msg}): ${surrogateSafeSlice(payload, 200)}`;\n }\n continue;\n }\n\n const eventType = parsed.event_type as string | undefined;\n if (!eventType) continue;\n\n if (eventType === \"content.delta\") {\n const delta = parsed.delta as Record<string, unknown> | undefined;\n if (!delta) continue;\n\n if (delta.type === \"text\" && typeof delta.text === \"string\") {\n content += delta.text;\n } else if (delta.type === \"function_call\") {\n toolCalls.push({\n name: String(delta.name ?? \"\"),\n arguments:\n typeof delta.arguments === \"string\"\n ? delta.arguments\n : JSON.stringify(delta.arguments ?? {}),\n ...(delta.id ? { id: String(delta.id) } : {}),\n });\n } else if (delta.type === \"thought_summary\" && typeof delta.text === \"string\") {\n reasoning += delta.text;\n }\n }\n }\n\n if (toolCalls.length > 0) {\n return {\n ...(content ? { content } : {}),\n toolCalls,\n ...(reasoning ? { reasoning } : {}),\n ...(droppedChunks > 0 ? { droppedChunks } : {}),\n ...(firstDroppedSample ? { firstDroppedSample } : {}),\n };\n }\n\n return {\n content,\n ...(reasoning ? { reasoning } : {}),\n ...(droppedChunks > 0 ? { droppedChunks } : {}),\n ...(firstDroppedSample ? { firstDroppedSample } : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Dispatch helper — pick the right collapse function by provider\n// ---------------------------------------------------------------------------\n\n/**\n * Collapse a streaming response body into a non-streaming fixture response.\n * Returns null if the content type is not a known streaming format.\n * Falls back to OpenAI SSE parsing for unrecognized provider keys with text/event-stream.\n */\nexport function collapseStreamingResponse(\n contentType: string,\n providerKey: RecordProviderKey,\n body: string | Buffer,\n logger?: Logger,\n): CollapseResult | null {\n const ct = contentType.toLowerCase();\n\n if (ct.includes(\"application/vnd.amazon.eventstream\")) {\n const buf = typeof body === \"string\" ? Buffer.from(body, \"binary\") : body;\n return collapseBedrockEventStream(buf);\n }\n\n if (ct.includes(\"application/x-ndjson\")) {\n const str = typeof body === \"string\" ? body : body.toString(\"utf8\");\n return collapseOllamaNDJSON(str);\n }\n\n if (ct.includes(\"text/event-stream\")) {\n const str = typeof body === \"string\" ? body : body.toString(\"utf8\");\n switch (providerKey) {\n case \"openai\":\n case \"azure\":\n return collapseOpenAISSE(str);\n case \"anthropic\":\n return collapseAnthropicSSE(str);\n case \"gemini\":\n case \"vertexai\":\n return collapseGeminiSSE(str);\n case \"gemini-interactions\":\n return collapseGeminiInteractionsSSE(str);\n case \"cohere\":\n return collapseCohereSSE(str);\n case \"bedrock\":\n return collapseAnthropicSSE(str);\n default:\n logger?.warn(\n `[stream-collapse] unknown SSE provider \"${providerKey}\", falling back to OpenAI SSE format`,\n );\n return collapseOpenAISSE(str);\n }\n }\n\n return null;\n}\n"],"mappings":"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"}
|
package/dist/transcription.cjs
CHANGED
|
@@ -53,7 +53,7 @@ async function handleTranscription(req, res, raw, fixtures, journal, defaults, s
|
|
|
53
53
|
_context: require_helpers.getContext(req)
|
|
54
54
|
};
|
|
55
55
|
const testId = require_helpers.getTestId(req);
|
|
56
|
-
const fixture = require_router.
|
|
56
|
+
const { fixture, skippedBySequenceOrTurn } = require_router.matchFixtureDiagnostic(fixtures, syntheticReq, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform);
|
|
57
57
|
if (fixture) {
|
|
58
58
|
journal.incrementFixtureMatchCount(fixture, fixtures, testId);
|
|
59
59
|
defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);
|
|
@@ -66,6 +66,8 @@ async function handleTranscription(req, res, raw, fixtures, journal, defaults, s
|
|
|
66
66
|
}, fixture ? "fixture" : "proxy", defaults.registry, defaults.logger)) return;
|
|
67
67
|
if (!fixture) {
|
|
68
68
|
if (require_helpers.resolveStrictMode(defaults.strict, req.headers)) {
|
|
69
|
+
const strictMessage = require_helpers.strictNoMatchMessage(skippedBySequenceOrTurn);
|
|
70
|
+
defaults.logger.error(require_helpers.strictNoMatchLogLine(method, path, skippedBySequenceOrTurn));
|
|
69
71
|
journal.add({
|
|
70
72
|
method,
|
|
71
73
|
path,
|
|
@@ -78,7 +80,7 @@ async function handleTranscription(req, res, raw, fixtures, journal, defaults, s
|
|
|
78
80
|
}
|
|
79
81
|
});
|
|
80
82
|
require_sse_writer.writeErrorResponse(res, 503, JSON.stringify({ error: {
|
|
81
|
-
message:
|
|
83
|
+
message: strictMessage,
|
|
82
84
|
type: "invalid_request_error",
|
|
83
85
|
code: "no_fixture_match"
|
|
84
86
|
} }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transcription.cjs","names":["getContext","getTestId","matchFixture","applyChaos","flattenHeaders","resolveStrictMode","strictOverrideField","proxyAndRecord","resolveResponse","isErrorResponse","serializeErrorResponse","isTranscriptionResponse"],"sources":["../src/transcription.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { ChatCompletionRequest, Fixture, HandlerDefaults } from \"./types.js\";\nimport {\n isTranscriptionResponse,\n isErrorResponse,\n serializeErrorResponse,\n flattenHeaders,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n getContext,\n} from \"./helpers.js\";\nimport { matchFixture } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaos } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\n/**\n * Extract the multipart boundary string from a Content-Type header.\n */\nexport function extractBoundary(contentType: string | undefined): string | undefined {\n if (!contentType) return undefined;\n const match = contentType.match(/boundary=([^\\s;]+)/i);\n return match?.[1];\n}\n\n/**\n * Extract a text field from multipart form data using boundary-based parsing.\n * Splits the body by the multipart boundary so each part is isolated, then\n * checks each part's Content-Disposition header for the target field name.\n * This avoids false matches from binary audio data that might contain\n * header-like byte sequences.\n */\nexport function extractFormField(\n raw: string,\n fieldName: string,\n boundary: string | undefined,\n): string | undefined {\n if (!boundary) {\n // Fallback: no boundary available, use simple regex (best-effort)\n console.warn(\"extractFormField: no multipart boundary found, using best-effort regex fallback\");\n const escaped = fieldName.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const pattern = new RegExp(\n `Content-Disposition:\\\\s*form-data;[^\\\\r\\\\n]*name=\"${escaped}\"[^\\\\r\\\\n]*\\\\r\\\\n\\\\r\\\\n([^\\\\r\\\\n]*)`,\n \"i\",\n );\n const match = raw.match(pattern);\n return match?.[1];\n }\n\n // Split by boundary delimiter — each chunk is one part\n const delimiter = `--${boundary}`;\n const parts = raw.split(delimiter);\n\n for (const part of parts) {\n // Skip the preamble (before first boundary) and epilogue (after closing boundary)\n if (!part || part.trimStart().startsWith(\"--\")) continue;\n\n // Split part into headers and body at the first blank line (\\r\\n\\r\\n)\n const headerEnd = part.indexOf(\"\\r\\n\\r\\n\");\n if (headerEnd === -1) continue;\n\n const headers = part.slice(0, headerEnd);\n const body = part.slice(headerEnd + 4);\n\n // Check if this part's Content-Disposition names the target field\n const cdMatch = headers.match(/Content-Disposition:\\s*form-data;[^\\r\\n]*name=\"([^\"]+)\"/i);\n if (cdMatch && cdMatch[1] === fieldName) {\n // Return the body value, trimming trailing \\r\\n from the part boundary\n return body.replace(/\\r\\n$/, \"\");\n }\n }\n return undefined;\n}\n\nexport async function handleTranscription(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n endpointType: \"transcription\" | \"translation\" = \"transcription\",\n): Promise<void> {\n setCorsHeaders(res);\n const defaultPath =\n endpointType === \"translation\" ? \"/v1/audio/translations\" : \"/v1/audio/transcriptions\";\n const path = req.url ?? defaultPath;\n const method = req.method ?? \"POST\";\n\n const contentType = Array.isArray(req.headers[\"content-type\"])\n ? req.headers[\"content-type\"][0]\n : req.headers[\"content-type\"];\n const boundary = extractBoundary(contentType);\n\n const model = extractFormField(raw, \"model\", boundary) ?? \"whisper-1\";\n const responseFormat = extractFormField(raw, \"response_format\", boundary) ?? \"json\";\n\n const syntheticReq: ChatCompletionRequest = {\n model,\n messages: [],\n _endpointType: endpointType,\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const fixture = matchFixture(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n defaults.logger.debug(`No fixture matched for request`);\n }\n\n if (\n applyChaos(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: syntheticReq },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (!fixture) {\n const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n if (effectiveStrict) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\n JSON.stringify({\n error: {\n message: \"Strict mode: no fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"openai\",\n req.url ?? defaultPath,\n fixtures,\n defaults,\n raw,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome !== \"not_configured\") {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return;\n }\n }\n\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 404,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 404,\n JSON.stringify({\n error: {\n message: \"No fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n\n const response = await resolveResponse(fixture, syntheticReq);\n\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, serializeErrorResponse(response), {\n retryAfter: response.retryAfter,\n });\n return;\n }\n\n if (!isTranscriptionResponse(response)) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message: \"Fixture response is not a transcription type\",\n type: \"server_error\",\n },\n }),\n );\n return;\n }\n\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n\n const t = response.transcription;\n const useVerbose = responseFormat === \"verbose_json\" || t.words != null || t.segments != null;\n\n if (useVerbose) {\n const verboseBody: Record<string, unknown> = {\n task: endpointType === \"translation\" ? \"translate\" : \"transcribe\",\n language: t.language ?? \"english\",\n duration: t.duration ?? 0,\n text: t.text,\n };\n if (t.words && t.words.length > 0) {\n verboseBody.words = t.words;\n }\n if (t.segments && t.segments.length > 0) {\n verboseBody.segments = t.segments;\n }\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(verboseBody));\n } else {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ text: t.text }));\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"transcription.cjs","names":["getContext","getTestId","matchFixtureDiagnostic","applyChaos","flattenHeaders","resolveStrictMode","strictNoMatchMessage","strictNoMatchLogLine","strictOverrideField","proxyAndRecord","resolveResponse","isErrorResponse","serializeErrorResponse","isTranscriptionResponse"],"sources":["../src/transcription.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { ChatCompletionRequest, Fixture, HandlerDefaults } from \"./types.js\";\nimport {\n isTranscriptionResponse,\n isErrorResponse,\n serializeErrorResponse,\n flattenHeaders,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n getContext,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaos } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\n/**\n * Extract the multipart boundary string from a Content-Type header.\n */\nexport function extractBoundary(contentType: string | undefined): string | undefined {\n if (!contentType) return undefined;\n const match = contentType.match(/boundary=([^\\s;]+)/i);\n return match?.[1];\n}\n\n/**\n * Extract a text field from multipart form data using boundary-based parsing.\n * Splits the body by the multipart boundary so each part is isolated, then\n * checks each part's Content-Disposition header for the target field name.\n * This avoids false matches from binary audio data that might contain\n * header-like byte sequences.\n */\nexport function extractFormField(\n raw: string,\n fieldName: string,\n boundary: string | undefined,\n): string | undefined {\n if (!boundary) {\n // Fallback: no boundary available, use simple regex (best-effort)\n console.warn(\"extractFormField: no multipart boundary found, using best-effort regex fallback\");\n const escaped = fieldName.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const pattern = new RegExp(\n `Content-Disposition:\\\\s*form-data;[^\\\\r\\\\n]*name=\"${escaped}\"[^\\\\r\\\\n]*\\\\r\\\\n\\\\r\\\\n([^\\\\r\\\\n]*)`,\n \"i\",\n );\n const match = raw.match(pattern);\n return match?.[1];\n }\n\n // Split by boundary delimiter — each chunk is one part\n const delimiter = `--${boundary}`;\n const parts = raw.split(delimiter);\n\n for (const part of parts) {\n // Skip the preamble (before first boundary) and epilogue (after closing boundary)\n if (!part || part.trimStart().startsWith(\"--\")) continue;\n\n // Split part into headers and body at the first blank line (\\r\\n\\r\\n)\n const headerEnd = part.indexOf(\"\\r\\n\\r\\n\");\n if (headerEnd === -1) continue;\n\n const headers = part.slice(0, headerEnd);\n const body = part.slice(headerEnd + 4);\n\n // Check if this part's Content-Disposition names the target field\n const cdMatch = headers.match(/Content-Disposition:\\s*form-data;[^\\r\\n]*name=\"([^\"]+)\"/i);\n if (cdMatch && cdMatch[1] === fieldName) {\n // Return the body value, trimming trailing \\r\\n from the part boundary\n return body.replace(/\\r\\n$/, \"\");\n }\n }\n return undefined;\n}\n\nexport async function handleTranscription(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n endpointType: \"transcription\" | \"translation\" = \"transcription\",\n): Promise<void> {\n setCorsHeaders(res);\n const defaultPath =\n endpointType === \"translation\" ? \"/v1/audio/translations\" : \"/v1/audio/transcriptions\";\n const path = req.url ?? defaultPath;\n const method = req.method ?? \"POST\";\n\n const contentType = Array.isArray(req.headers[\"content-type\"])\n ? req.headers[\"content-type\"][0]\n : req.headers[\"content-type\"];\n const boundary = extractBoundary(contentType);\n\n const model = extractFormField(raw, \"model\", boundary) ?? \"whisper-1\";\n const responseFormat = extractFormField(raw, \"response_format\", boundary) ?? \"json\";\n\n const syntheticReq: ChatCompletionRequest = {\n model,\n messages: [],\n _endpointType: endpointType,\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n defaults.logger.debug(`No fixture matched for request`);\n }\n\n if (\n applyChaos(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: syntheticReq },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (!fixture) {\n const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n if (effectiveStrict) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n defaults.logger.error(strictNoMatchLogLine(method, path, skippedBySequenceOrTurn));\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\n JSON.stringify({\n error: {\n message: strictMessage,\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"openai\",\n req.url ?? defaultPath,\n fixtures,\n defaults,\n raw,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome !== \"not_configured\") {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return;\n }\n }\n\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 404,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 404,\n JSON.stringify({\n error: {\n message: \"No fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n\n const response = await resolveResponse(fixture, syntheticReq);\n\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, serializeErrorResponse(response), {\n retryAfter: response.retryAfter,\n });\n return;\n }\n\n if (!isTranscriptionResponse(response)) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message: \"Fixture response is not a transcription type\",\n type: \"server_error\",\n },\n }),\n );\n return;\n }\n\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n\n const t = response.transcription;\n const useVerbose = responseFormat === \"verbose_json\" || t.words != null || t.segments != null;\n\n if (useVerbose) {\n const verboseBody: Record<string, unknown> = {\n task: endpointType === \"translation\" ? \"translate\" : \"transcribe\",\n language: t.language ?? \"english\",\n duration: t.duration ?? 0,\n text: t.text,\n };\n if (t.words && t.words.length > 0) {\n verboseBody.words = t.words;\n }\n if (t.segments && t.segments.length > 0) {\n verboseBody.segments = t.segments;\n }\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(verboseBody));\n } else {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ text: t.text }));\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transcription.d.cts","names":[],"sources":["../src/transcription.ts"],"sourcesContent":[],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"transcription.d.cts","names":[],"sources":["../src/transcription.ts"],"sourcesContent":[],"mappings":";;;;;;iBA+EsB,mBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,uCAEA,oBACD,mBACC,uCACY,MAAA,CAAK,0EAE1B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transcription.d.ts","names":[],"sources":["../src/transcription.ts"],"sourcesContent":[],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"transcription.d.ts","names":[],"sources":["../src/transcription.ts"],"sourcesContent":[],"mappings":";;;;;;iBA+EsB,mBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,uCAEA,oBACD,mBACC,uCACY,MAAA,CAAK,0EAE1B"}
|
package/dist/transcription.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { flattenHeaders, getContext, getTestId, isErrorResponse, isTranscriptionResponse, resolveResponse, resolveStrictMode, serializeErrorResponse, strictOverrideField } from "./helpers.js";
|
|
2
|
-
import {
|
|
1
|
+
import { flattenHeaders, getContext, getTestId, isErrorResponse, isTranscriptionResponse, resolveResponse, resolveStrictMode, serializeErrorResponse, strictNoMatchLogLine, strictNoMatchMessage, strictOverrideField } from "./helpers.js";
|
|
2
|
+
import { matchFixtureDiagnostic } from "./router.js";
|
|
3
3
|
import { writeErrorResponse } from "./sse-writer.js";
|
|
4
4
|
import { applyChaos } from "./chaos.js";
|
|
5
5
|
import { proxyAndRecord } from "./recorder.js";
|
|
@@ -53,7 +53,7 @@ async function handleTranscription(req, res, raw, fixtures, journal, defaults, s
|
|
|
53
53
|
_context: getContext(req)
|
|
54
54
|
};
|
|
55
55
|
const testId = getTestId(req);
|
|
56
|
-
const fixture =
|
|
56
|
+
const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(fixtures, syntheticReq, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform);
|
|
57
57
|
if (fixture) {
|
|
58
58
|
journal.incrementFixtureMatchCount(fixture, fixtures, testId);
|
|
59
59
|
defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);
|
|
@@ -66,6 +66,8 @@ async function handleTranscription(req, res, raw, fixtures, journal, defaults, s
|
|
|
66
66
|
}, fixture ? "fixture" : "proxy", defaults.registry, defaults.logger)) return;
|
|
67
67
|
if (!fixture) {
|
|
68
68
|
if (resolveStrictMode(defaults.strict, req.headers)) {
|
|
69
|
+
const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);
|
|
70
|
+
defaults.logger.error(strictNoMatchLogLine(method, path, skippedBySequenceOrTurn));
|
|
69
71
|
journal.add({
|
|
70
72
|
method,
|
|
71
73
|
path,
|
|
@@ -78,7 +80,7 @@ async function handleTranscription(req, res, raw, fixtures, journal, defaults, s
|
|
|
78
80
|
}
|
|
79
81
|
});
|
|
80
82
|
writeErrorResponse(res, 503, JSON.stringify({ error: {
|
|
81
|
-
message:
|
|
83
|
+
message: strictMessage,
|
|
82
84
|
type: "invalid_request_error",
|
|
83
85
|
code: "no_fixture_match"
|
|
84
86
|
} }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transcription.js","names":[],"sources":["../src/transcription.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { ChatCompletionRequest, Fixture, HandlerDefaults } from \"./types.js\";\nimport {\n isTranscriptionResponse,\n isErrorResponse,\n serializeErrorResponse,\n flattenHeaders,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n getContext,\n} from \"./helpers.js\";\nimport { matchFixture } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaos } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\n/**\n * Extract the multipart boundary string from a Content-Type header.\n */\nexport function extractBoundary(contentType: string | undefined): string | undefined {\n if (!contentType) return undefined;\n const match = contentType.match(/boundary=([^\\s;]+)/i);\n return match?.[1];\n}\n\n/**\n * Extract a text field from multipart form data using boundary-based parsing.\n * Splits the body by the multipart boundary so each part is isolated, then\n * checks each part's Content-Disposition header for the target field name.\n * This avoids false matches from binary audio data that might contain\n * header-like byte sequences.\n */\nexport function extractFormField(\n raw: string,\n fieldName: string,\n boundary: string | undefined,\n): string | undefined {\n if (!boundary) {\n // Fallback: no boundary available, use simple regex (best-effort)\n console.warn(\"extractFormField: no multipart boundary found, using best-effort regex fallback\");\n const escaped = fieldName.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const pattern = new RegExp(\n `Content-Disposition:\\\\s*form-data;[^\\\\r\\\\n]*name=\"${escaped}\"[^\\\\r\\\\n]*\\\\r\\\\n\\\\r\\\\n([^\\\\r\\\\n]*)`,\n \"i\",\n );\n const match = raw.match(pattern);\n return match?.[1];\n }\n\n // Split by boundary delimiter — each chunk is one part\n const delimiter = `--${boundary}`;\n const parts = raw.split(delimiter);\n\n for (const part of parts) {\n // Skip the preamble (before first boundary) and epilogue (after closing boundary)\n if (!part || part.trimStart().startsWith(\"--\")) continue;\n\n // Split part into headers and body at the first blank line (\\r\\n\\r\\n)\n const headerEnd = part.indexOf(\"\\r\\n\\r\\n\");\n if (headerEnd === -1) continue;\n\n const headers = part.slice(0, headerEnd);\n const body = part.slice(headerEnd + 4);\n\n // Check if this part's Content-Disposition names the target field\n const cdMatch = headers.match(/Content-Disposition:\\s*form-data;[^\\r\\n]*name=\"([^\"]+)\"/i);\n if (cdMatch && cdMatch[1] === fieldName) {\n // Return the body value, trimming trailing \\r\\n from the part boundary\n return body.replace(/\\r\\n$/, \"\");\n }\n }\n return undefined;\n}\n\nexport async function handleTranscription(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n endpointType: \"transcription\" | \"translation\" = \"transcription\",\n): Promise<void> {\n setCorsHeaders(res);\n const defaultPath =\n endpointType === \"translation\" ? \"/v1/audio/translations\" : \"/v1/audio/transcriptions\";\n const path = req.url ?? defaultPath;\n const method = req.method ?? \"POST\";\n\n const contentType = Array.isArray(req.headers[\"content-type\"])\n ? req.headers[\"content-type\"][0]\n : req.headers[\"content-type\"];\n const boundary = extractBoundary(contentType);\n\n const model = extractFormField(raw, \"model\", boundary) ?? \"whisper-1\";\n const responseFormat = extractFormField(raw, \"response_format\", boundary) ?? \"json\";\n\n const syntheticReq: ChatCompletionRequest = {\n model,\n messages: [],\n _endpointType: endpointType,\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const fixture = matchFixture(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n defaults.logger.debug(`No fixture matched for request`);\n }\n\n if (\n applyChaos(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: syntheticReq },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (!fixture) {\n const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n if (effectiveStrict) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\n JSON.stringify({\n error: {\n message: \"Strict mode: no fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"openai\",\n req.url ?? defaultPath,\n fixtures,\n defaults,\n raw,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome !== \"not_configured\") {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return;\n }\n }\n\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 404,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 404,\n JSON.stringify({\n error: {\n message: \"No fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n\n const response = await resolveResponse(fixture, syntheticReq);\n\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, serializeErrorResponse(response), {\n retryAfter: response.retryAfter,\n });\n return;\n }\n\n if (!isTranscriptionResponse(response)) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message: \"Fixture response is not a transcription type\",\n type: \"server_error\",\n },\n }),\n );\n return;\n }\n\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n\n const t = response.transcription;\n const useVerbose = responseFormat === \"verbose_json\" || t.words != null || t.segments != null;\n\n if (useVerbose) {\n const verboseBody: Record<string, unknown> = {\n task: endpointType === \"translation\" ? \"translate\" : \"transcribe\",\n language: t.language ?? \"english\",\n duration: t.duration ?? 0,\n text: t.text,\n };\n if (t.words && t.words.length > 0) {\n verboseBody.words = t.words;\n }\n if (t.segments && t.segments.length > 0) {\n verboseBody.segments = t.segments;\n }\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(verboseBody));\n } else {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ text: t.text }));\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"transcription.js","names":[],"sources":["../src/transcription.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { ChatCompletionRequest, Fixture, HandlerDefaults } from \"./types.js\";\nimport {\n isTranscriptionResponse,\n isErrorResponse,\n serializeErrorResponse,\n flattenHeaders,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n getContext,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaos } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\n/**\n * Extract the multipart boundary string from a Content-Type header.\n */\nexport function extractBoundary(contentType: string | undefined): string | undefined {\n if (!contentType) return undefined;\n const match = contentType.match(/boundary=([^\\s;]+)/i);\n return match?.[1];\n}\n\n/**\n * Extract a text field from multipart form data using boundary-based parsing.\n * Splits the body by the multipart boundary so each part is isolated, then\n * checks each part's Content-Disposition header for the target field name.\n * This avoids false matches from binary audio data that might contain\n * header-like byte sequences.\n */\nexport function extractFormField(\n raw: string,\n fieldName: string,\n boundary: string | undefined,\n): string | undefined {\n if (!boundary) {\n // Fallback: no boundary available, use simple regex (best-effort)\n console.warn(\"extractFormField: no multipart boundary found, using best-effort regex fallback\");\n const escaped = fieldName.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const pattern = new RegExp(\n `Content-Disposition:\\\\s*form-data;[^\\\\r\\\\n]*name=\"${escaped}\"[^\\\\r\\\\n]*\\\\r\\\\n\\\\r\\\\n([^\\\\r\\\\n]*)`,\n \"i\",\n );\n const match = raw.match(pattern);\n return match?.[1];\n }\n\n // Split by boundary delimiter — each chunk is one part\n const delimiter = `--${boundary}`;\n const parts = raw.split(delimiter);\n\n for (const part of parts) {\n // Skip the preamble (before first boundary) and epilogue (after closing boundary)\n if (!part || part.trimStart().startsWith(\"--\")) continue;\n\n // Split part into headers and body at the first blank line (\\r\\n\\r\\n)\n const headerEnd = part.indexOf(\"\\r\\n\\r\\n\");\n if (headerEnd === -1) continue;\n\n const headers = part.slice(0, headerEnd);\n const body = part.slice(headerEnd + 4);\n\n // Check if this part's Content-Disposition names the target field\n const cdMatch = headers.match(/Content-Disposition:\\s*form-data;[^\\r\\n]*name=\"([^\"]+)\"/i);\n if (cdMatch && cdMatch[1] === fieldName) {\n // Return the body value, trimming trailing \\r\\n from the part boundary\n return body.replace(/\\r\\n$/, \"\");\n }\n }\n return undefined;\n}\n\nexport async function handleTranscription(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n endpointType: \"transcription\" | \"translation\" = \"transcription\",\n): Promise<void> {\n setCorsHeaders(res);\n const defaultPath =\n endpointType === \"translation\" ? \"/v1/audio/translations\" : \"/v1/audio/transcriptions\";\n const path = req.url ?? defaultPath;\n const method = req.method ?? \"POST\";\n\n const contentType = Array.isArray(req.headers[\"content-type\"])\n ? req.headers[\"content-type\"][0]\n : req.headers[\"content-type\"];\n const boundary = extractBoundary(contentType);\n\n const model = extractFormField(raw, \"model\", boundary) ?? \"whisper-1\";\n const responseFormat = extractFormField(raw, \"response_format\", boundary) ?? \"json\";\n\n const syntheticReq: ChatCompletionRequest = {\n model,\n messages: [],\n _endpointType: endpointType,\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n defaults.logger.debug(`No fixture matched for request`);\n }\n\n if (\n applyChaos(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: syntheticReq },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (!fixture) {\n const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n if (effectiveStrict) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n defaults.logger.error(strictNoMatchLogLine(method, path, skippedBySequenceOrTurn));\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\n JSON.stringify({\n error: {\n message: strictMessage,\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"openai\",\n req.url ?? defaultPath,\n fixtures,\n defaults,\n raw,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome !== \"not_configured\") {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return;\n }\n }\n\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 404,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 404,\n JSON.stringify({\n error: {\n message: \"No fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n\n const response = await resolveResponse(fixture, syntheticReq);\n\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, serializeErrorResponse(response), {\n retryAfter: response.retryAfter,\n });\n return;\n }\n\n if (!isTranscriptionResponse(response)) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message: \"Fixture response is not a transcription type\",\n type: \"server_error\",\n },\n }),\n );\n return;\n }\n\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n\n const t = response.transcription;\n const useVerbose = responseFormat === \"verbose_json\" || t.words != null || t.segments != null;\n\n if (useVerbose) {\n const verboseBody: Record<string, unknown> = {\n task: endpointType === \"translation\" ? \"translate\" : \"transcribe\",\n language: t.language ?? \"english\",\n duration: t.duration ?? 0,\n text: t.text,\n };\n if (t.words && t.words.length > 0) {\n verboseBody.words = t.words;\n }\n if (t.segments && t.segments.length > 0) {\n verboseBody.segments = t.segments;\n }\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(verboseBody));\n } else {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ text: t.text }));\n }\n}\n"],"mappings":"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"}
|
package/dist/types.d.cts
CHANGED
|
@@ -134,6 +134,26 @@ interface ResponseOverrides {
|
|
|
134
134
|
interface TextResponse extends ResponseOverrides {
|
|
135
135
|
content: string;
|
|
136
136
|
reasoning?: string;
|
|
137
|
+
/**
|
|
138
|
+
* The real cryptographic `signature` captured from a recorded Anthropic
|
|
139
|
+
* thinking turn. When present it is emitted on the replayed thinking block;
|
|
140
|
+
* otherwise replay falls back to aimock's round-trip-safe placeholder.
|
|
141
|
+
* Persisted only alongside a non-empty `reasoning`: a signature captured
|
|
142
|
+
* without plaintext reasoning (e.g. whitespace-only thinking) is intentionally
|
|
143
|
+
* discarded at the persistence layer, since replay attaches signatures only to
|
|
144
|
+
* plaintext thinking blocks.
|
|
145
|
+
*/
|
|
146
|
+
reasoningSignature?: string;
|
|
147
|
+
/**
|
|
148
|
+
* Opaque `data` payload(s) of any Anthropic `redacted_thinking` blocks
|
|
149
|
+
* captured from a recorded thinking turn, in stream order. When present they
|
|
150
|
+
* are replayed as faithful `redacted_thinking` content blocks so the encrypted
|
|
151
|
+
* reasoning round-trips; absent for non-Anthropic providers and turns without
|
|
152
|
+
* redacted thinking. Recorded redacted blocks replay as a leading group: the
|
|
153
|
+
* original interleaving of `thinking` and `redacted_thinking` blocks is not
|
|
154
|
+
* preserved (see the CHANGELOG fidelity caveat).
|
|
155
|
+
*/
|
|
156
|
+
redactedThinking?: string[];
|
|
137
157
|
webSearches?: string[];
|
|
138
158
|
}
|
|
139
159
|
interface ToolCall {
|
|
@@ -143,12 +163,21 @@ interface ToolCall {
|
|
|
143
163
|
}
|
|
144
164
|
interface ToolCallResponse extends ResponseOverrides {
|
|
145
165
|
toolCalls: ToolCall[];
|
|
166
|
+
reasoning?: string;
|
|
167
|
+
/** Real Anthropic thinking-block signature; see {@link TextResponse.reasoningSignature}. */
|
|
168
|
+
reasoningSignature?: string;
|
|
169
|
+
/** Anthropic redacted_thinking block data; see {@link TextResponse.redactedThinking}. */
|
|
170
|
+
redactedThinking?: string[];
|
|
146
171
|
webSearches?: string[];
|
|
147
172
|
}
|
|
148
173
|
interface ContentWithToolCallsResponse extends ResponseOverrides {
|
|
149
174
|
content: string;
|
|
150
175
|
toolCalls: ToolCall[];
|
|
151
176
|
reasoning?: string;
|
|
177
|
+
/** Real Anthropic thinking-block signature; see {@link TextResponse.reasoningSignature}. */
|
|
178
|
+
reasoningSignature?: string;
|
|
179
|
+
/** Anthropic redacted_thinking block data; see {@link TextResponse.redactedThinking}. */
|
|
180
|
+
redactedThinking?: string[];
|
|
152
181
|
webSearches?: string[];
|
|
153
182
|
}
|
|
154
183
|
interface ErrorResponse {
|
|
@@ -282,12 +311,21 @@ interface FixtureFileToolCall {
|
|
|
282
311
|
}
|
|
283
312
|
interface FixtureFileToolCallResponse extends ResponseOverrides {
|
|
284
313
|
toolCalls: FixtureFileToolCall[];
|
|
314
|
+
reasoning?: string;
|
|
315
|
+
/** Real Anthropic thinking-block signature; see {@link TextResponse.reasoningSignature}. */
|
|
316
|
+
reasoningSignature?: string;
|
|
317
|
+
/** Anthropic redacted_thinking block data; see {@link TextResponse.redactedThinking}. */
|
|
318
|
+
redactedThinking?: string[];
|
|
285
319
|
webSearches?: string[];
|
|
286
320
|
}
|
|
287
321
|
interface FixtureFileTextResponse extends ResponseOverrides {
|
|
288
322
|
/** Accepts a JSON object or array (structured output) — the loader will JSON.stringify it. */
|
|
289
323
|
content: string | Record<string, unknown> | unknown[];
|
|
290
324
|
reasoning?: string;
|
|
325
|
+
/** Real Anthropic thinking-block signature; see {@link TextResponse.reasoningSignature}. */
|
|
326
|
+
reasoningSignature?: string;
|
|
327
|
+
/** Anthropic redacted_thinking block data; see {@link TextResponse.redactedThinking}. */
|
|
328
|
+
redactedThinking?: string[];
|
|
291
329
|
webSearches?: string[];
|
|
292
330
|
}
|
|
293
331
|
interface FixtureFileContentWithToolCallsResponse extends ResponseOverrides {
|
|
@@ -295,6 +333,10 @@ interface FixtureFileContentWithToolCallsResponse extends ResponseOverrides {
|
|
|
295
333
|
content: string | Record<string, unknown> | unknown[];
|
|
296
334
|
toolCalls: FixtureFileToolCall[];
|
|
297
335
|
reasoning?: string;
|
|
336
|
+
/** Real Anthropic thinking-block signature; see {@link TextResponse.reasoningSignature}. */
|
|
337
|
+
reasoningSignature?: string;
|
|
338
|
+
/** Anthropic redacted_thinking block data; see {@link TextResponse.redactedThinking}. */
|
|
339
|
+
redactedThinking?: string[];
|
|
298
340
|
webSearches?: string[];
|
|
299
341
|
}
|
|
300
342
|
type FixtureFileResponse = FixtureFileTextResponse | FixtureFileToolCallResponse | FixtureFileContentWithToolCallsResponse | ErrorResponse | EmbeddingResponse | ImageResponse | AudioResponse | TranscriptionResponse | VideoResponse | RawJSONResponse;
|