@copilotkit/aimock 1.44.0 → 1.45.1

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.
Files changed (278) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +42 -0
  4. package/README.md +6 -0
  5. package/dist/agui-types.d.cts.map +1 -1
  6. package/dist/aimock-cli.cjs +31 -5
  7. package/dist/aimock-cli.cjs.map +1 -1
  8. package/dist/aimock-cli.d.cts.map +1 -1
  9. package/dist/aimock-cli.d.ts.map +1 -1
  10. package/dist/aimock-cli.js +31 -5
  11. package/dist/aimock-cli.js.map +1 -1
  12. package/dist/bedrock-converse.cjs +312 -39
  13. package/dist/bedrock-converse.cjs.map +1 -1
  14. package/dist/bedrock-converse.d.cts.map +1 -1
  15. package/dist/bedrock-converse.d.ts.map +1 -1
  16. package/dist/bedrock-converse.js +313 -40
  17. package/dist/bedrock-converse.js.map +1 -1
  18. package/dist/bedrock.cjs +195 -42
  19. package/dist/bedrock.cjs.map +1 -1
  20. package/dist/bedrock.d.cts.map +1 -1
  21. package/dist/bedrock.d.ts.map +1 -1
  22. package/dist/bedrock.js +196 -43
  23. package/dist/bedrock.js.map +1 -1
  24. package/dist/chaos.cjs +1 -0
  25. package/dist/chaos.js +1 -1
  26. package/dist/cli.cjs +152 -5
  27. package/dist/cli.cjs.map +1 -1
  28. package/dist/cli.js +155 -8
  29. package/dist/cli.js.map +1 -1
  30. package/dist/cohere.cjs +132 -71
  31. package/dist/cohere.cjs.map +1 -1
  32. package/dist/cohere.d.cts.map +1 -1
  33. package/dist/cohere.d.ts.map +1 -1
  34. package/dist/cohere.js +133 -72
  35. package/dist/cohere.js.map +1 -1
  36. package/dist/config-loader.cjs +62 -4
  37. package/dist/config-loader.cjs.map +1 -1
  38. package/dist/config-loader.d.cts +37 -4
  39. package/dist/config-loader.d.cts.map +1 -1
  40. package/dist/config-loader.d.ts +37 -4
  41. package/dist/config-loader.d.ts.map +1 -1
  42. package/dist/config-loader.js +62 -4
  43. package/dist/config-loader.js.map +1 -1
  44. package/dist/fakes-plugin.cjs +131 -0
  45. package/dist/fakes-plugin.cjs.map +1 -0
  46. package/dist/fakes-plugin.d.cts +23 -0
  47. package/dist/fakes-plugin.d.cts.map +1 -0
  48. package/dist/fakes-plugin.d.ts +23 -0
  49. package/dist/fakes-plugin.d.ts.map +1 -0
  50. package/dist/fakes-plugin.js +127 -0
  51. package/dist/fakes-plugin.js.map +1 -0
  52. package/dist/fixture-loader-services.cjs +1 -1
  53. package/dist/fixture-loader-services.cjs.map +1 -1
  54. package/dist/fixture-loader-services.js +1 -1
  55. package/dist/fixture-loader-services.js.map +1 -1
  56. package/dist/fixture-loader.cjs +389 -27
  57. package/dist/fixture-loader.cjs.map +1 -1
  58. package/dist/fixture-loader.d.cts +47 -3
  59. package/dist/fixture-loader.d.cts.map +1 -1
  60. package/dist/fixture-loader.d.ts +47 -3
  61. package/dist/fixture-loader.d.ts.map +1 -1
  62. package/dist/fixture-loader.js +384 -29
  63. package/dist/fixture-loader.js.map +1 -1
  64. package/dist/gemini-interactions.cjs +250 -52
  65. package/dist/gemini-interactions.cjs.map +1 -1
  66. package/dist/gemini-interactions.d.cts.map +1 -1
  67. package/dist/gemini-interactions.d.ts.map +1 -1
  68. package/dist/gemini-interactions.js +251 -53
  69. package/dist/gemini-interactions.js.map +1 -1
  70. package/dist/gemini.cjs +226 -37
  71. package/dist/gemini.cjs.map +1 -1
  72. package/dist/gemini.d.cts.map +1 -1
  73. package/dist/gemini.d.ts.map +1 -1
  74. package/dist/gemini.js +227 -38
  75. package/dist/gemini.js.map +1 -1
  76. package/dist/helpers.cjs +881 -7
  77. package/dist/helpers.cjs.map +1 -1
  78. package/dist/helpers.d.cts +85 -4
  79. package/dist/helpers.d.cts.map +1 -1
  80. package/dist/helpers.d.ts +85 -4
  81. package/dist/helpers.d.ts.map +1 -1
  82. package/dist/helpers.js +839 -8
  83. package/dist/helpers.js.map +1 -1
  84. package/dist/index.cjs +9 -0
  85. package/dist/index.d.cts +7 -5
  86. package/dist/index.d.ts +7 -5
  87. package/dist/index.js +4 -3
  88. package/dist/jest.cjs +31 -7
  89. package/dist/jest.cjs.map +1 -1
  90. package/dist/jest.d.cts +43 -3
  91. package/dist/jest.d.cts.map +1 -1
  92. package/dist/jest.d.ts +43 -3
  93. package/dist/jest.d.ts.map +1 -1
  94. package/dist/jest.js +33 -9
  95. package/dist/jest.js.map +1 -1
  96. package/dist/journal.cjs +135 -13
  97. package/dist/journal.cjs.map +1 -1
  98. package/dist/journal.d.cts +38 -4
  99. package/dist/journal.d.cts.map +1 -1
  100. package/dist/journal.d.ts +38 -4
  101. package/dist/journal.d.ts.map +1 -1
  102. package/dist/journal.js +135 -13
  103. package/dist/journal.js.map +1 -1
  104. package/dist/live-sanitize.cjs +4 -32
  105. package/dist/live-sanitize.cjs.map +1 -1
  106. package/dist/live-sanitize.js +4 -32
  107. package/dist/live-sanitize.js.map +1 -1
  108. package/dist/llmock.cjs +123 -30
  109. package/dist/llmock.cjs.map +1 -1
  110. package/dist/llmock.d.cts +16 -1
  111. package/dist/llmock.d.cts.map +1 -1
  112. package/dist/llmock.d.ts +16 -1
  113. package/dist/llmock.d.ts.map +1 -1
  114. package/dist/llmock.js +125 -32
  115. package/dist/llmock.js.map +1 -1
  116. package/dist/mcp-fakes-mount.cjs +1 -0
  117. package/dist/mcp-fakes-mount.cjs.map +1 -1
  118. package/dist/mcp-fakes-mount.js +1 -0
  119. package/dist/mcp-fakes-mount.js.map +1 -1
  120. package/dist/mcp-fakes-report.cjs +111 -0
  121. package/dist/mcp-fakes-report.cjs.map +1 -0
  122. package/dist/mcp-fakes-report.d.cts +40 -0
  123. package/dist/mcp-fakes-report.d.cts.map +1 -0
  124. package/dist/mcp-fakes-report.d.ts +40 -0
  125. package/dist/mcp-fakes-report.d.ts.map +1 -0
  126. package/dist/mcp-fakes-report.js +107 -0
  127. package/dist/mcp-fakes-report.js.map +1 -0
  128. package/dist/mcp-fakes.cjs +331 -8
  129. package/dist/mcp-fakes.cjs.map +1 -1
  130. package/dist/mcp-fakes.d.cts +406 -3
  131. package/dist/mcp-fakes.d.cts.map +1 -1
  132. package/dist/mcp-fakes.d.ts +406 -3
  133. package/dist/mcp-fakes.d.ts.map +1 -1
  134. package/dist/mcp-fakes.js +331 -8
  135. package/dist/mcp-fakes.js.map +1 -1
  136. package/dist/mcp-handler.cjs +216 -17
  137. package/dist/mcp-handler.cjs.map +1 -1
  138. package/dist/mcp-handler.d.cts +49 -0
  139. package/dist/mcp-handler.d.cts.map +1 -0
  140. package/dist/mcp-handler.d.ts +49 -0
  141. package/dist/mcp-handler.d.ts.map +1 -0
  142. package/dist/mcp-handler.js +214 -18
  143. package/dist/mcp-handler.js.map +1 -1
  144. package/dist/mcp-mock.cjs +112 -1
  145. package/dist/mcp-mock.cjs.map +1 -1
  146. package/dist/mcp-mock.d.cts +33 -2
  147. package/dist/mcp-mock.d.cts.map +1 -1
  148. package/dist/mcp-mock.d.ts +33 -2
  149. package/dist/mcp-mock.d.ts.map +1 -1
  150. package/dist/mcp-mock.js +112 -1
  151. package/dist/mcp-mock.js.map +1 -1
  152. package/dist/mcp-recorder.cjs +1224 -0
  153. package/dist/mcp-recorder.cjs.map +1 -0
  154. package/dist/mcp-recorder.d.cts +108 -0
  155. package/dist/mcp-recorder.d.cts.map +1 -0
  156. package/dist/mcp-recorder.d.ts +108 -0
  157. package/dist/mcp-recorder.d.ts.map +1 -0
  158. package/dist/mcp-recorder.js +1215 -0
  159. package/dist/mcp-recorder.js.map +1 -0
  160. package/dist/messages.cjs +362 -56
  161. package/dist/messages.cjs.map +1 -1
  162. package/dist/messages.d.cts.map +1 -1
  163. package/dist/messages.d.ts.map +1 -1
  164. package/dist/messages.js +363 -58
  165. package/dist/messages.js.map +1 -1
  166. package/dist/misbehavior.cjs +1523 -0
  167. package/dist/misbehavior.cjs.map +1 -0
  168. package/dist/misbehavior.d.cts +43 -0
  169. package/dist/misbehavior.d.cts.map +1 -0
  170. package/dist/misbehavior.d.ts +43 -0
  171. package/dist/misbehavior.d.ts.map +1 -0
  172. package/dist/misbehavior.js +1511 -0
  173. package/dist/misbehavior.js.map +1 -0
  174. package/dist/ollama.cjs +310 -112
  175. package/dist/ollama.cjs.map +1 -1
  176. package/dist/ollama.d.cts.map +1 -1
  177. package/dist/ollama.d.ts.map +1 -1
  178. package/dist/ollama.js +311 -113
  179. package/dist/ollama.js.map +1 -1
  180. package/dist/openrouter-video.cjs +30 -2
  181. package/dist/openrouter-video.cjs.map +1 -1
  182. package/dist/openrouter-video.d.cts.map +1 -1
  183. package/dist/openrouter-video.d.ts.map +1 -1
  184. package/dist/openrouter-video.js +30 -2
  185. package/dist/openrouter-video.js.map +1 -1
  186. package/dist/record-sanitize.cjs +231 -0
  187. package/dist/record-sanitize.cjs.map +1 -0
  188. package/dist/record-sanitize.js +224 -0
  189. package/dist/record-sanitize.js.map +1 -0
  190. package/dist/recorder.cjs +106 -5
  191. package/dist/recorder.cjs.map +1 -1
  192. package/dist/recorder.d.cts +2 -1
  193. package/dist/recorder.d.cts.map +1 -1
  194. package/dist/recorder.d.ts +2 -1
  195. package/dist/recorder.d.ts.map +1 -1
  196. package/dist/recorder.js +107 -7
  197. package/dist/recorder.js.map +1 -1
  198. package/dist/responses.cjs +667 -171
  199. package/dist/responses.cjs.map +1 -1
  200. package/dist/responses.d.cts +5 -13
  201. package/dist/responses.d.cts.map +1 -1
  202. package/dist/responses.d.ts +5 -13
  203. package/dist/responses.d.ts.map +1 -1
  204. package/dist/responses.js +665 -174
  205. package/dist/responses.js.map +1 -1
  206. package/dist/router.cjs +24 -2
  207. package/dist/router.cjs.map +1 -1
  208. package/dist/router.d.cts.map +1 -1
  209. package/dist/router.d.ts.map +1 -1
  210. package/dist/router.js +25 -4
  211. package/dist/router.js.map +1 -1
  212. package/dist/server.cjs +572 -76
  213. package/dist/server.cjs.map +1 -1
  214. package/dist/server.d.cts.map +1 -1
  215. package/dist/server.d.ts.map +1 -1
  216. package/dist/server.js +574 -78
  217. package/dist/server.js.map +1 -1
  218. package/dist/stream-collapse.cjs +236 -34
  219. package/dist/stream-collapse.cjs.map +1 -1
  220. package/dist/stream-collapse.d.cts +39 -4
  221. package/dist/stream-collapse.d.cts.map +1 -1
  222. package/dist/stream-collapse.d.ts +39 -4
  223. package/dist/stream-collapse.d.ts.map +1 -1
  224. package/dist/stream-collapse.js +235 -35
  225. package/dist/stream-collapse.js.map +1 -1
  226. package/dist/types.cjs.map +1 -1
  227. package/dist/types.d.cts +315 -4
  228. package/dist/types.d.cts.map +1 -1
  229. package/dist/types.d.ts +315 -4
  230. package/dist/types.d.ts.map +1 -1
  231. package/dist/types.js.map +1 -1
  232. package/dist/validate-cli.cjs +36 -4
  233. package/dist/validate-cli.cjs.map +1 -1
  234. package/dist/validate-cli.js +37 -5
  235. package/dist/validate-cli.js.map +1 -1
  236. package/dist/vector-types.d.ts.map +1 -1
  237. package/dist/version.cjs +45 -0
  238. package/dist/version.cjs.map +1 -0
  239. package/dist/version.js +44 -0
  240. package/dist/version.js.map +1 -0
  241. package/dist/vitest.cjs +66 -10
  242. package/dist/vitest.cjs.map +1 -1
  243. package/dist/vitest.d.cts +35 -4
  244. package/dist/vitest.d.cts.map +1 -1
  245. package/dist/vitest.d.ts +35 -4
  246. package/dist/vitest.d.ts.map +1 -1
  247. package/dist/vitest.js +67 -11
  248. package/dist/vitest.js.map +1 -1
  249. package/dist/watcher.cjs +6 -2
  250. package/dist/watcher.cjs.map +1 -1
  251. package/dist/watcher.js +6 -2
  252. package/dist/watcher.js.map +1 -1
  253. package/dist/ws-gemini-live.cjs +210 -101
  254. package/dist/ws-gemini-live.cjs.map +1 -1
  255. package/dist/ws-gemini-live.d.cts +9 -3
  256. package/dist/ws-gemini-live.d.cts.map +1 -1
  257. package/dist/ws-gemini-live.d.ts +9 -3
  258. package/dist/ws-gemini-live.d.ts.map +1 -1
  259. package/dist/ws-gemini-live.js +211 -102
  260. package/dist/ws-gemini-live.js.map +1 -1
  261. package/dist/ws-realtime.cjs +175 -62
  262. package/dist/ws-realtime.cjs.map +1 -1
  263. package/dist/ws-realtime.d.cts +8 -3
  264. package/dist/ws-realtime.d.cts.map +1 -1
  265. package/dist/ws-realtime.d.ts +8 -3
  266. package/dist/ws-realtime.d.ts.map +1 -1
  267. package/dist/ws-realtime.js +176 -63
  268. package/dist/ws-realtime.js.map +1 -1
  269. package/dist/ws-responses.cjs +109 -16
  270. package/dist/ws-responses.cjs.map +1 -1
  271. package/dist/ws-responses.d.cts +8 -12
  272. package/dist/ws-responses.d.cts.map +1 -1
  273. package/dist/ws-responses.d.ts +8 -12
  274. package/dist/ws-responses.d.ts.map +1 -1
  275. package/dist/ws-responses.js +111 -18
  276. package/dist/ws-responses.js.map +1 -1
  277. package/package.json +10 -2
  278. package/skills/write-fixtures/SKILL.md +179 -21
@@ -1 +1 @@
1
- {"version":3,"file":"recorder.js","names":["crypto","http"],"sources":["../src/recorder.ts"],"sourcesContent":["import * as http from \"node:http\";\nimport * as https from \"node:https\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as crypto from \"node:crypto\";\nimport { StringDecoder } from \"node:string_decoder\";\nimport type {\n ChatCompletionRequest,\n Fixture,\n FixtureMatch,\n FixtureResponse,\n RecordConfig,\n RecordedTimings,\n RecordProviderKey,\n ResponseOverrides,\n ToolCall,\n} from \"./types.js\";\nimport { getLastMessageByRole, getTextContent, currentTurnHasToolResult } from \"./router.js\";\nimport { normalizeModelName } from \"./model-utils.js\";\nimport type { Logger } from \"./logger.js\";\nimport { collapseStreamingResponse, capturedRedactedData } from \"./stream-collapse.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport { resolveUpstreamUrl } from \"./url.js\";\nimport { applyConfiguredProviderAuth, applyProviderAuth } from \"./provider-auth.js\";\nimport { isAuthenticatedRequest, isRecognizedApiKeyHeader } from \"./api-key-auth.js\";\nimport { getTestId, slugifyTestId, slugifyContext, hasMintedRequestId } from \"./helpers.js\";\nimport { DEFAULT_TEST_ID } from \"./constants.js\";\n\n/** True when an SSE frame completes an OpenAI stream. */\nfunction isTerminalSSEFrame(frame: string): boolean {\n const data = frame\n .split(/\\r?\\n/)\n .filter((line) => line.startsWith(\"data:\"))\n .map((line) => line.slice(\"data:\".length).trimStart())\n .join(\"\\n\")\n .trim();\n\n if (data === \"[DONE]\") return true;\n\n try {\n return (JSON.parse(data) as { type?: unknown }).type === \"transcript.text.done\";\n } catch {\n return false;\n }\n}\n\n/** Headers to strip when proxying — hop-by-hop (RFC 2616 §13.5.1) + client-set. */\n/**\n * Default ceiling (bytes) for the in-memory proxy-path buffer. Chosen well\n * under V8's ~512 MiB max string length so `rawBuffer.toString()` cannot throw\n * `RangeError: Invalid string length`, and so a single huge proxied response\n * cannot spike the heap unbounded. Overridable via\n * `RecordConfig.maxProxyBufferBytes` / `--max-proxy-buffer-bytes`.\n *\n * NOTE: this byte cap bounds the RAW BUFFER only. `stream-collapse` accumulates\n * its own per-channel strings (`content`/`reasoning`/tool `arguments`/etc.) from\n * that buffer and is ALSO reachable directly (exported from index.ts), so it\n * carries its OWN independent guard (`MAX_COLLAPSE_STRING_LENGTH`) rather than\n * relying on this cap — an earlier version of this comment wrongly asserted\n * stream-collapse \"never throws Invalid string length\", which was false for the\n * unbounded `content += delta` accumulators (the ~1/sec prod RangeError).\n */\nexport const DEFAULT_MAX_PROXY_BUFFER_BYTES = 64 * 1024 * 1024; // 64 MiB\n\n/**\n * Default ceiling for the number of SSE/NDJSON/EventStream frames whose\n * per-frame state (`frameTimestamps`, parse buffers) aimock retains for a\n * single proxied response. Frame state is count-indexed, not byte-sized, so a\n * long-lived / never-ending stream accumulates `frameTimestamps` entries (and,\n * if a frame never completes, parse-buffer bytes) UNBOUNDED even when the byte\n * cap is generous — observed as multi-GB heap growth over many hours from a few\n * long nested-sub-agent streams. Tripping truncation on EITHER bytes OR frame\n * count bounds both. 5M frames is generous for any real response (a normal\n * completion is hundreds-to-thousands of frames) while still bounding a runaway\n * stream to ~tens of MB of frame state. Overridable via\n * `RecordConfig.maxProxyBufferFrames` / `--max-proxy-buffer-frames`.\n */\nexport const DEFAULT_MAX_PROXY_BUFFER_FRAMES = 5_000_000;\n\n/**\n * Absolute hard ceiling (bytes) for any in-memory proxy buffer, independent of\n * the configurable `maxProxyBufferBytes`. V8's maximum STRING length on 64-bit\n * is 2^29 - 1 (~512 MiB of UTF-16 code units), and the proxy buffer is\n * eventually stringified via `rawBuffer.toString()` for collapse/relay — so the\n * BYTE buffer must stay safely under that string limit or the toString throws\n * `RangeError: Invalid string length`. 256 MiB of bytes is well under the\n * ~512 MiB string-length boundary (these are different units — bytes vs UTF-16\n * code units — but 256 MiB of bytes can never decode to more than 256 Mi code\n * units, comfortably below 2^29 - 1). Used to clamp the configurable cap AND to\n * bound the non-progressive relay buffer that must be retained past a cap trip.\n */\nexport const PROXY_BUFFER_HARD_CEILING = 256 * 1024 * 1024; // 256 MiB\n\n/**\n * Test-only override of the effective hard ceiling. Lets the proxy-buffer\n * enforcement suite exercise the >hard-ceiling fail-loud path with a small\n * body instead of streaming 256 MiB. `undefined` (the default) uses the real\n * `PROXY_BUFFER_HARD_CEILING`. NEVER set from production code.\n */\nlet proxyBufferHardCeilingOverride: number | undefined;\n\n/** @internal test-only — see `proxyBufferHardCeilingOverride`. */\nexport function setProxyBufferHardCeilingForTests(value: number | undefined): void {\n proxyBufferHardCeilingOverride = value;\n}\n\n/** Effective hard ceiling, honoring any active test-only override. */\nfunction effectiveHardCeiling(): number {\n return proxyBufferHardCeilingOverride ?? PROXY_BUFFER_HARD_CEILING;\n}\n\nconst STRIP_HEADERS = new Set([\n // Hop-by-hop (RFC 2616 §13.5.1)\n \"connection\",\n \"keep-alive\",\n \"transfer-encoding\",\n \"te\",\n \"trailer\",\n \"upgrade\",\n \"proxy-authorization\",\n \"proxy-authenticate\",\n // Set by HTTP client from the target URL / body\n \"host\",\n \"content-length\",\n // Not relevant for LLM APIs; avoid leaking or mismatched encoding\n \"cookie\",\n \"accept-encoding\",\n // Mock-internal control headers — meaningless (and potentially confusing\n // or leaky) on a real provider's wire. x-aimock-chaos-* is stripped by\n // prefix in buildForwardHeaders below.\n \"x-test-id\",\n \"x-aimock-strict\",\n \"x-aimock-context\",\n]);\n\n/**\n * Headers stripped on egress ONLY when aimock minted the value. A\n * caller-supplied `x-request-id` is the caller's own trace key and forwards\n * verbatim; the id aimock invents for its access log and journal is\n * meaningless on a real provider's wire and must not be transmitted.\n */\nconst MINTED_ONLY_STRIP_HEADERS = new Set([\"x-request-id\"]);\n\n/**\n * Build the header set forwarded to an upstream provider from an incoming\n * request: everything except hop-by-hop, client-set, and mock-internal\n * headers (STRIP_HEADERS, plus the x-aimock-chaos-* prefix family, plus\n * MINTED_ONLY_STRIP_HEADERS when aimock minted the value). Shared\n * by the generic recorder proxy and the OpenRouter-video live lifecycle\n * proxy.\n */\nexport function buildForwardHeaders(req: http.IncomingMessage): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [name, val] of Object.entries(req.headers)) {\n const lower = name.toLowerCase();\n if (\n val === undefined ||\n STRIP_HEADERS.has(lower) ||\n (MINTED_ONLY_STRIP_HEADERS.has(lower) && hasMintedRequestId(req)) ||\n lower.startsWith(\"x-aimock-chaos-\") ||\n (isAuthenticatedRequest(req) && isRecognizedApiKeyHeader(lower))\n ) {\n continue;\n }\n out[name] = Array.isArray(val) ? val.join(\", \") : val;\n }\n return out;\n}\n\n/** Remove every casing of a header from an egress map. */\nexport function removeForwardHeader(headers: Record<string, string>, name: string): void {\n const lowerName = name.toLowerCase();\n for (const key of Object.keys(headers)) {\n if (key.toLowerCase() === lowerName) delete headers[key];\n }\n}\n\n/**\n * Set a header on an egress map ONLY if the inbound request did not already\n * supply it (in any casing). Used for content-negotiation defaults the mock\n * wants when the caller was not explicit. Never overwrites: a caller-sent\n * header may be covered by a request signature (AWS SigV4 and friends sign a\n * named subset of headers), so replacing one silently invalidates the request\n * upstream.\n */\nexport function setForwardHeaderDefault(\n headers: Record<string, string>,\n name: string,\n value: string,\n): void {\n const lowerName = name.toLowerCase();\n for (const key of Object.keys(headers)) {\n if (key.toLowerCase() === lowerName) return;\n }\n headers[name] = value;\n}\n\n/**\n * Construct the only safe egress map when inbound access control is enabled.\n * Test credentials are always removed; a static configured provider credential\n * is mandatory so an accepted test credential can never become an upstream one.\n */\nexport function prepareEgressHeaders(\n req: http.IncomingMessage,\n target: URL,\n providerKey: RecordProviderKey,\n configuredKey: string | undefined,\n): Record<string, string> | undefined {\n const headers = buildForwardHeaders(req);\n if (!isAuthenticatedRequest(req)) {\n applyProviderAuth(headers, target, providerKey, configuredKey);\n return headers;\n }\n for (const name of Object.keys(headers)) {\n if (isRecognizedApiKeyHeader(name)) delete headers[name];\n }\n return configuredKey && applyConfiguredProviderAuth(headers, target, providerKey, configuredKey)\n ? headers\n : undefined;\n}\n\n/**\n * Captured upstream response, exposed to the `beforeWriteResponse` hook so\n * callers can decide whether to relay it or mutate it (e.g. chaos injection).\n */\nexport interface ProxyCapturedResponse {\n status: number;\n contentType: string;\n body: Buffer;\n}\n\nexport interface ProxyOptions {\n /**\n * Called after the upstream response has been captured and recorded, but\n * before the relay to the client. Contract when the hook returns `true`:\n * 1. It wrote its own response body on `res`.\n * 2. It journaled the outcome (proxyAndRecord will NOT journal it).\n * 3. proxyAndRecord skips its default relay and returns `\"handled_by_hook\"`.\n *\n * Returning `false` (or omitting the hook) lets proxyAndRecord relay the\n * upstream response normally and leaves journaling to the caller via the\n * `\"relayed\"` outcome. Rejected promises propagate and leave the response\n * unwritten.\n *\n * NOT invoked when the upstream response was streamed progressively to the\n * client (SSE, NDJSON, or binary event streams) — the bytes are already on\n * the wire and can't be mutated.\n * Callers that need to observe the bypass should pass `onHookBypassed`.\n */\n beforeWriteResponse?: (response: ProxyCapturedResponse) => boolean | Promise<boolean>;\n /**\n * Called when `beforeWriteResponse` was provided but could not be invoked\n * because the upstream response was streamed to the client progressively.\n * The hook was rolled + wired but the bytes left before it could fire.\n * Intended for observability (log/metric/journal annotation) — proxyAndRecord\n * still returns `\"relayed\"`.\n */\n onHookBypassed?: (reason: \"sse_streamed\" | \"ndjson_streamed\" | \"binary_streamed\") => void;\n}\n\n/**\n * Outcome of a proxyAndRecord call, returned so the caller can decide whether\n * to journal, fall through, or stop — without sharing a mutable flag with the\n * `beforeWriteResponse` hook.\n *\n * - `\"not_configured\"` — no upstream URL for this provider; caller should fall\n * through to its next branch (typically strict/404).\n * - `\"relayed\"` — the default code path wrote a response (upstream success or\n * synthesized 502 error). Caller should journal the outcome.\n * - `\"handled_by_hook\"` — the hook wrote + journaled its own response. Caller\n * should not double-journal.\n */\nexport type ProxyOutcome = \"not_configured\" | \"relayed\" | \"handled_by_hook\";\n\n/**\n * Result of `persistFixture`:\n * - `\"skipped\"` — proxy-only mode; the caller has nothing else to do.\n * - `\"written\"` — fixture saved to `filepath` and (unless the match was empty)\n * registered into the in-memory cache so the next identical request matches.\n * - `\"failed\"` — filesystem write failed. Caller decides how to surface it\n * (e.g. setting `X-AIMock-Record-Error` on a relay response).\n */\nexport type PersistFixtureResult =\n | { kind: \"skipped\" }\n | { kind: \"written\"; filepath: string }\n | { kind: \"failed\"; error: string };\n\n/**\n * Make an arbitrary string safe to set as an HTTP header value. Node's\n * `res.setHeader` throws ERR_INVALID_CHAR on anything outside Latin-1 (plus\n * control characters) — and persist errors embed filesystem paths verbatim,\n * so a Unicode fixture path would turn a recoverable record failure into a\n * 500. Out-of-range characters are replaced with `?` rather than stripped so\n * the value's shape stays legible.\n */\nexport function sanitizeHeaderValue(value: string): string {\n return value.replace(/[^\\t\\x20-\\x7e\\x80-\\xff]/g, \"?\");\n}\n\n/**\n * Resolve a `content-type` header value to a single string. Node's\n * `http.IncomingHttpHeaders` types `content-type` as `string | string[]`, and\n * a constructed/proxied header object CAN carry an array (e.g. duplicated\n * header lines surfaced by some HTTP stacks). `join(\", \")`-ing such an array\n * produces a malformed `Content-Type: application/json, text/html` value — pick\n * the FIRST element instead, which is the value the client should act on.\n * Returns `\"\"` when the header is absent or an empty array.\n */\nexport function pickContentType(contentType: string | string[] | undefined): string {\n if (Array.isArray(contentType)) {\n return contentType.length > 0 ? contentType[0] : \"\";\n }\n return contentType ?? \"\";\n}\n\n/**\n * Write a built fixture to disk (snapshot vs. timestamp file layout) and, when\n * the match is non-empty, register it in the in-memory cache so subsequent\n * identical requests match. Extracted from `proxyAndRecord` so the fal\n * queue-walk recorder (which makes multiple upstream calls before knowing the\n * final body) can share the same persistence behavior without re-implementing\n * snapshot-mode merging and warnings.\n */\nexport function persistFixture(opts: {\n record: RecordConfig;\n providerKey: RecordProviderKey;\n testId: string;\n /** Separate Live captures from HTTP OpenAI snapshots. */\n filePrefix?: \"openai-live\";\n fixture: Fixture;\n fixtures: Fixture[];\n warnings?: string[];\n logger: Logger;\n}): PersistFixtureResult {\n const { record, providerKey, testId, fixture, fixtures, warnings = [], logger } = opts;\n const fileKey = opts.filePrefix ?? providerKey;\n\n // Match criteria with no userMessage / inputText / endpoint will not match\n // any future request — warn, then save to disk for inspection but skip the\n // in-memory registration so a defective fixture doesn't shadow real ones.\n // turnIndex/hasToolResult are pure multi-turn disambiguators on their own.\n const m = fixture.match;\n const isEmptyMatch =\n m.userMessage === undefined && m.inputText === undefined && m.endpoint === undefined;\n\n if (record.proxyOnly) {\n logger.info(`Proxied ${providerKey} request (proxy-only mode)`);\n return { kind: \"skipped\" };\n }\n\n // Warned only past the proxy-only early-return: under proxy-only nothing is\n // persisted or registered, so an empty match has no consequence there.\n if (isEmptyMatch) {\n logger.warn(\"Recorded fixture has empty match criteria — skipping in-memory registration\");\n }\n\n const fixturePath = record.fixturePath ?? \"./fixtures/recorded\";\n let isSnapshotMode = testId !== DEFAULT_TEST_ID;\n let filepath: string;\n let mergeExisting = false;\n\n if (isSnapshotMode) {\n const slug = slugifyTestId(testId);\n if (!slug) {\n // Slug resolved to empty (e.g. testId was all punctuation) — fall back\n // to timestamp-based recording so we still capture the fixture.\n isSnapshotMode = false;\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n filepath = path.join(\n fixturePath,\n `${fileKey}-${timestamp}-${crypto.randomUUID().slice(0, 8)}.json`,\n );\n } else {\n filepath = path.join(fixturePath, slug, `${fileKey}.json`);\n mergeExisting = true;\n }\n } else {\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const timestampFile = `${fileKey}-${timestamp}-${crypto.randomUUID().slice(0, 8)}.json`;\n // The context becomes a directory segment, but it originates from the\n // attacker-controllable `X-AIMock-Context` header — slugify it (mirroring\n // testId above) so `../`, separators, and absolute prefixes can't escape\n // the fixtures base dir. An empty slug → no context segment.\n const contextSegment = fixture.match.context\n ? slugifyContext(fixture.match.context)\n : undefined;\n filepath = contextSegment\n ? path.join(fixturePath, contextSegment, timestampFile)\n : path.join(fixturePath, timestampFile);\n }\n\n const fileWarnings = [\n ...(isEmptyMatch ? [\"Empty match criteria — this fixture will not match any request\"] : []),\n ...warnings,\n ];\n\n let tmpPath: string | undefined;\n try {\n fs.mkdirSync(path.dirname(filepath), { recursive: true });\n\n // Auth headers are forwarded to upstream but excluded from saved fixtures.\n // The persisted fixture is always the real upstream response, even when\n // chaos later mutates the relay; replay must see what upstream said.\n // Warnings are persisted as a `_warnings` JSON array (authoritative,\n // non-fragmenting) AND a legacy \"; \"-joined `_warning` string (kept for\n // backward-compatible readers). The array is the source of truth on a\n // snapshot merge: a single warning that itself contains \"; \" (e.g.\n // \"captured A; then B\") would be fragmented into two bogus entries if the\n // joined string were split back apart on merge. Carrying the array forward\n // avoids that round-trip fragmentation entirely.\n let fileContent: {\n [key: string]: unknown;\n fixtures: unknown[];\n _warning?: string;\n _warnings?: string[];\n };\n if (mergeExisting && fs.existsSync(filepath)) {\n try {\n const existing = JSON.parse(fs.readFileSync(filepath, \"utf-8\"));\n // Guard the spread: a non-array `fixtures` (e.g. a string from a\n // hand-edited file) would silently spread into single characters and\n // mangle the merged file. Treat it like the corrupt-JSON case below.\n const existingFixtures = Array.isArray(existing.fixtures)\n ? (existing.fixtures as unknown[])\n : undefined;\n if (existingFixtures === undefined && existing.fixtures !== undefined) {\n logger.warn(\n `Existing fixture file ${filepath} has a non-array \"fixtures\" — discarding it and starting fresh`,\n );\n }\n // Keep every top-level key the recorder does not own (e.g. a\n // hand-added `mcpFakes` block). The recorder owns `fixtures`,\n // `_warnings` and `_warning`; those are rebuilt below, so a stale\n // copy is never carried over verbatim.\n const keptKeys: Record<string, unknown> = {};\n if (existing !== null && typeof existing === \"object\" && !Array.isArray(existing)) {\n for (const [key, value] of Object.entries(existing as Record<string, unknown>)) {\n if (key === \"fixtures\" || key === \"_warnings\" || key === \"_warning\") continue;\n keptKeys[key] = value;\n }\n }\n fileContent = { ...keptKeys, fixtures: [...(existingFixtures ?? []), fixture] };\n // Carry existing warnings forward — a later clean capture merging into\n // the same snapshot file must not erase an earlier capture's warning\n // (e.g. an over-cap b64 omission). Prefer the new `_warnings` array;\n // fall back to the legacy \"; \"-joined `_warning` string for files\n // written before this format change. The legacy split can still\n // fragment an embedded-\"; \" warning in a pre-existing file, but new\n // captures no longer create that hazard.\n if (Array.isArray(existing._warnings)) {\n fileWarnings.unshift(\n ...(existing._warnings as unknown[]).filter((w): w is string => typeof w === \"string\"),\n );\n } else if (typeof existing._warning === \"string\" && existing._warning) {\n fileWarnings.unshift(...existing._warning.split(\"; \"));\n }\n } catch (mergeErr) {\n const msg = mergeErr instanceof Error ? mergeErr.message : \"unknown\";\n logger.warn(`Could not read existing fixture file ${filepath} (${msg}) — overwriting`);\n fileContent = { fixtures: [fixture] };\n }\n } else {\n fileContent = { fixtures: [fixture] };\n }\n if (fileWarnings.length > 0) {\n // Exact-duplicate warnings (e.g. repeated over-cap captures merging into\n // the same snapshot file) collapse to one entry. Emit BOTH the\n // authoritative array and the legacy joined string.\n const dedupedWarnings = [...new Set(fileWarnings)];\n fileContent._warnings = dedupedWarnings;\n fileContent._warning = dedupedWarnings.join(\"; \");\n }\n // Atomic write: write to temp file then rename to avoid read-modify-write\n // races. Keep synchronous — for streamed responses the HTTP reply is\n // already on the wire, so async writes would race with callers checking\n // the filesystem before the fixture has landed.\n tmpPath = filepath + \".tmp.\" + crypto.randomUUID();\n fs.writeFileSync(tmpPath, JSON.stringify(fileContent, null, 2), {\n encoding: \"utf-8\",\n flag: \"wx\",\n mode: 0o600,\n });\n fs.renameSync(tmpPath, filepath);\n tmpPath = undefined;\n\n if (!isEmptyMatch) {\n fixtures.push(fixture);\n }\n logger.warn(`Response recorded → ${filepath}`);\n return { kind: \"written\", filepath };\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown filesystem error\";\n logger.error(`Failed to save fixture to disk: ${msg}`);\n return { kind: \"failed\", error: msg };\n } finally {\n if (tmpPath) {\n try {\n fs.unlinkSync(tmpPath);\n } catch {\n /* Preserve the original persistence error. */\n }\n }\n }\n}\n\n/**\n * Proxy an unmatched request to the real upstream provider, record the\n * response as a fixture on disk and in memory, then relay the response\n * back to the original client.\n */\nexport async function proxyAndRecord(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n request: ChatCompletionRequest,\n providerKey: RecordProviderKey,\n pathname: string,\n fixtures: Fixture[],\n defaults: {\n record?: RecordConfig;\n logger: Logger;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n },\n rawBody?: string,\n options?: ProxyOptions,\n): Promise<ProxyOutcome> {\n const record = defaults.record;\n if (!record) return \"not_configured\";\n\n const providers = record.providers;\n // Gemini Interactions uses the same upstream API as Gemini (identical base URL\n // and auth), so we remap the provider key to reuse the configured Gemini URL.\n const lookupKey = providerKey === \"gemini-interactions\" ? \"gemini\" : providerKey;\n const upstreamUrl = providers[lookupKey];\n\n if (!upstreamUrl) {\n defaults.logger.warn(`No upstream URL configured for provider \"${providerKey}\" — cannot proxy`);\n return \"not_configured\";\n }\n\n let target: URL;\n try {\n target = resolveUpstreamUrl(upstreamUrl, pathname);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n defaults.logger.error(\n `Invalid upstream URL for provider \"${providerKey}\": ${upstreamUrl} (${msg})`,\n );\n writeErrorResponse(\n res,\n 502,\n JSON.stringify({\n error: { message: `Invalid upstream URL: ${upstreamUrl}`, type: \"proxy_error\" },\n }),\n );\n return \"relayed\";\n }\n\n defaults.logger.warn(`NO FIXTURE MATCH — proxying to ${upstreamUrl}${pathname}`);\n\n // Forward all request headers except hop-by-hop and client-set ones.\n const forwardHeaders = prepareEgressHeaders(\n req,\n target,\n providerKey,\n record.providerKeys?.[lookupKey],\n );\n if (!forwardHeaders) {\n writeErrorResponse(\n res,\n 502,\n JSON.stringify({\n error: { message: \"No configured provider credential\", type: \"proxy_error\" },\n }),\n );\n return \"relayed\";\n }\n\n const requestBody = rawBody ?? JSON.stringify(request);\n\n // Make upstream request\n let upstreamStatus: number;\n let upstreamHeaders: http.IncomingHttpHeaders;\n let upstreamBody: string;\n let rawBuffer: Buffer;\n let bufferTruncated = false;\n let truncationCap: \"byte\" | \"frame\" | undefined;\n let totalBytes = 0;\n let hardCeilingExceeded = false;\n\n // Track whether we streamed SSE progressively to the client; if so,\n // skip the final res.writeHead/res.end relay at the bottom of this fn.\n let streamedToClient = false;\n let clientDisconnected = false;\n let sawDone = false;\n let frameTimestamps: number[] = [];\n let streamStartTime = 0;\n const maxProxyBufferBytes = clampMaxBufferBytes(record.maxProxyBufferBytes);\n const maxProxyBufferFrames = clampMaxBufferFrames(record.maxProxyBufferFrames);\n try {\n const result = await makeUpstreamRequest(\n target,\n forwardHeaders,\n requestBody,\n res,\n req.method,\n defaults.logger,\n { upstreamTimeoutMs: record.upstreamTimeoutMs, bodyTimeoutMs: record.bodyTimeoutMs },\n maxProxyBufferBytes,\n maxProxyBufferFrames,\n );\n upstreamStatus = result.status;\n upstreamHeaders = result.headers;\n upstreamBody = result.body;\n rawBuffer = result.rawBuffer;\n streamedToClient = result.streamedToClient;\n clientDisconnected = result.clientDisconnected;\n sawDone = result.sawDone;\n frameTimestamps = result.frameTimestamps;\n streamStartTime = result.streamStartTime;\n bufferTruncated = result.bufferTruncated;\n truncationCap = result.truncationCap;\n totalBytes = result.totalBytes;\n hardCeilingExceeded = result.hardCeilingExceeded;\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown proxy error\";\n defaults.logger.error(`Proxy request failed: ${msg}`);\n if (!res.headersSent) {\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: { message: `Proxy to upstream failed: ${msg}`, type: \"proxy_error\" },\n }),\n );\n } else {\n // Streaming headers (200) are already on the wire, so we cannot change the\n // status — but we MUST NOT call res.end(), which the client reads as a\n // clean EOF and treats as a complete (silently truncated) response.\n // Destroy the connection instead so the client sees an aborted stream and\n // can surface the upstream failure rather than acting on partial data.\n res.destroy();\n }\n return \"relayed\";\n }\n\n // Buffer cap tripped: skip collapse + recording (the in-memory buffer can't\n // be safely/faithfully journaled), but ALWAYS deliver a faithful response to\n // the client. The cap means \"don't journal\", not \"don't answer\".\n // - PROGRESSIVE stream: the bytes were already teed to the client live, so\n // nothing more to write — just end if still open.\n // - NON-progressive response, UNDER the hard ceiling: makeUpstreamRequest\n // kept the FULL body (bounded by PROXY_BUFFER_HARD_CEILING) precisely so we\n // can relay it here. Relay the real body so a large single-shot JSON\n // (embeddings / fal / image-b64) reaches the client intact — but NORMALIZE\n // the status (success→200 / error→502) exactly like every other relay path\n // so upstream provider details don't leak through this branch alone.\n // - NON-progressive response, OVER the hard ceiling: the retained buffer is\n // PARTIAL (we stopped buffering at the ceiling and there is no live tee),\n // so relaying it would present a truncated body as success. FAIL LOUD with\n // a 502 instead — never deliver a silently-truncated body as 2xx.\n if (bufferTruncated) {\n const capDetail =\n truncationCap === \"frame\"\n ? `the ${maxProxyBufferFrames}-frame cap`\n : `the ${maxProxyBufferBytes}-byte cap`;\n if (!streamedToClient && hardCeilingExceeded && !res.headersSent) {\n const ceilingMiB = Math.round(PROXY_BUFFER_HARD_CEILING / (1024 * 1024));\n defaults.logger.error(\n `Upstream response exceeded the ${ceilingMiB} MiB proxy hard ceiling (saw ${totalBytes} bytes) on a non-progressive body — cannot relay the full body and refusing to relay a truncated one; returning 502`,\n );\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: `Upstream response exceeds ${ceilingMiB}MiB proxy ceiling`,\n type: \"proxy_error\",\n },\n }),\n );\n return \"relayed\";\n }\n defaults.logger.warn(\n `Upstream response exceeded ${capDetail} (saw ${totalBytes} bytes) — relayed to client, recording skipped`,\n );\n if (!streamedToClient && !res.headersSent) {\n // Normalize the relayed status like the under-cap relay paths\n // (success→200, error→502) so this branch does not leak a raw upstream\n // 429/503/etc. The full real body (under the hard ceiling) is relayed.\n const clientStatus = upstreamStatus >= 200 && upstreamStatus < 300 ? 200 : 502;\n const relayHeaders: Record<string, string> = {};\n const ct = upstreamHeaders[\"content-type\"];\n const ctStr = Array.isArray(ct) ? ct.join(\", \") : (ct ?? \"\");\n relayHeaders[\"Content-Type\"] = ctStr || \"application/json\";\n res.writeHead(clientStatus, relayHeaders);\n res.end(rawBuffer);\n } else if (!res.writableEnded) {\n res.end();\n }\n return \"relayed\";\n }\n\n // Detect streaming response and collapse if necessary.\n // NOTE: collapse buffers the upstream body in memory up to maxProxyBufferBytes\n // (see makeUpstreamRequest). Over-cap responses short-circuit above, so the\n // buffer reaching here is bounded and safe to stringify/collapse.\n const contentType = upstreamHeaders[\"content-type\"];\n const ctString = pickContentType(contentType);\n const isBinaryStream = ctString.toLowerCase().includes(\"application/vnd.amazon.eventstream\");\n const collapsed = collapseStreamingResponse(\n ctString,\n providerKey,\n isBinaryStream ? rawBuffer : upstreamBody,\n defaults.logger,\n );\n\n let fixtureResponse: FixtureResponse;\n\n // TTS response — binary audio, not JSON. Classify on the audio content-type\n // when upstream succeeded (2xx), regardless of body length: a zero-length\n // audio body is still an audio response (empty audio), NOT an opaque\n // proxy_error — letting it fall through to the JSON path would mis-record it.\n // A non-2xx audio response (error JSON in the body) is left to the JSON/error\n // path below.\n const isAudioResponse = ctString.toLowerCase().startsWith(\"audio/\");\n const isAudioSuccess = upstreamStatus >= 200 && upstreamStatus < 300;\n if (isAudioResponse && isAudioSuccess) {\n // Derive format from Content-Type (audio/mpeg→mp3, audio/opus→opus, etc.)\n const audioFormat = ctString\n .toLowerCase()\n .replace(\"audio/\", \"\")\n .replace(\"mpeg\", \"mp3\")\n .split(\";\")[0]\n .trim();\n if (rawBuffer.length === 0) {\n defaults.logger.warn(\n \"Audio response had a zero-length body — recording an empty audio fixture\",\n );\n }\n fixtureResponse = {\n audio: rawBuffer.toString(\"base64\"),\n ...(audioFormat && audioFormat !== \"mp3\" ? { format: audioFormat } : {}),\n };\n } else if (collapsed) {\n // Streaming response — use collapsed result\n defaults.logger.warn(`Streaming response detected (${ctString}) — collapsing to fixture`);\n if (collapsed.truncated) {\n defaults.logger.warn(\"Bedrock EventStream: CRC mismatch — response may be truncated\");\n }\n if (collapsed.droppedChunks && collapsed.droppedChunks > 0) {\n defaults.logger.warn(\n `${collapsed.droppedChunks} chunk(s) dropped during stream collapse${collapsed.firstDroppedSample ? ` — first: ${collapsed.firstDroppedSample}` : \"\"}`,\n );\n }\n if (collapsed.harmonyUnparsed) {\n defaults.logger.warn(\n `Harmony tokens present but unparseable — content preserved verbatim${collapsed.harmonyNote ? ` (${collapsed.harmonyNote})` : \"\"}`,\n );\n }\n // Provider-reported usage captured from the stream's final usage frame\n // (#368). Persisted on the text / toolCall fixture shapes so replay serves\n // the REAL token counts instead of the `ceil(len/4)` estimate, and so\n // OpenRouter's `usage.cost` round-trips to the replayed usage chunk /\n // completion envelope. Omitted when the stream carried no usage, keeping\n // every pre-#368 recorded fixture byte-identical. Transcription fixtures\n // carry their own `usage` slot, so the transcription branch below is left\n // untouched. The audio shape (`AudioResponse` in types.ts) has NO usage\n // field, so the audio branch cannot carry `usage` at all — inert today (a\n // collapse never yields both `audioB64` and a stream usage frame), but a\n // future collapser that set both would drop the usage on the audio path\n // with no compiler signal.\n const collapsedUsage = sanitizeRecordedUsage(collapsed.usage);\n const usageSpread = collapsedUsage ? { usage: collapsedUsage } : {};\n // Audio from streamed inlineData (e.g. Gemini SSE with audio parts).\n // A single Gemini turn can interleave audio with a functionCall and/or\n // text/thought parts; preserve those companion modalities so the tool call\n // / content / reasoning are not silently dropped when audio is present.\n if (collapsed.transcription) {\n fixtureResponse = { transcription: collapsed.transcription };\n } else if (collapsed.audioB64) {\n const audioToolCallsSpread =\n collapsed.toolCalls && collapsed.toolCalls.length > 0\n ? {\n toolCalls: collapsed.toolCalls.map((tc) => ({\n ...tc,\n name: tc.name ?? \"\",\n arguments: tc.arguments ?? \"{}\",\n })),\n }\n : {};\n const audioContentSpread = collapsed.content ? { content: collapsed.content } : {};\n const audioReasoningSpread = collapsed.reasoning ? { reasoning: collapsed.reasoning } : {};\n fixtureResponse = {\n audio: {\n b64Json: collapsed.audioB64,\n contentType: collapsed.audioMimeType ?? \"audio/mpeg\",\n },\n ...audioToolCallsSpread,\n ...audioContentSpread,\n ...audioReasoningSpread,\n };\n } else if (\n collapsed.content === \"\" &&\n (!collapsed.toolCalls || collapsed.toolCalls.length === 0)\n ) {\n defaults.logger.warn(\"Stream collapse produced empty content — fixture may be incomplete\");\n const reasoningSpread = collapsed.reasoning ? { reasoning: collapsed.reasoning } : {};\n // Carry the real Anthropic thinking-block signature only when reasoning is\n // also present (a bare signature has nothing to attach to on replay), so a\n // recorded thinking turn replays its actual signature instead of the\n // round-trip-safe placeholder.\n const reasoningSignatureSpread =\n collapsed.reasoning && collapsed.reasoningSignature\n ? { reasoningSignature: collapsed.reasoningSignature }\n : {};\n logDroppedReasoningSignature(\n defaults.logger,\n collapsed.reasoning,\n collapsed.reasoningSignature,\n );\n // Redacted-thinking blocks carry their OWN encrypted reasoning, so they are\n // carried independently of any plaintext `reasoning` (a turn can have only\n // redacted thinking) so the recorded turn round-trips its redacted blocks.\n const redactedThinkingSpread = collapsed.redactedThinking?.length\n ? { redactedThinking: collapsed.redactedThinking }\n : {};\n const webSearchesSpread = collapsed.webSearches?.length\n ? { webSearches: collapsed.webSearches }\n : {};\n fixtureResponse = {\n content: collapsed.content ?? \"\",\n ...reasoningSpread,\n ...reasoningSignatureSpread,\n ...redactedThinkingSpread,\n ...webSearchesSpread,\n ...usageSpread,\n };\n } else {\n const reasoningSpread = collapsed.reasoning ? { reasoning: collapsed.reasoning } : {};\n // Carry the real Anthropic thinking-block signature only when reasoning is\n // also present; see the empty-content branch above.\n const reasoningSignatureSpread =\n collapsed.reasoning && collapsed.reasoningSignature\n ? { reasoningSignature: collapsed.reasoningSignature }\n : {};\n logDroppedReasoningSignature(\n defaults.logger,\n collapsed.reasoning,\n collapsed.reasoningSignature,\n );\n // Redacted-thinking blocks carry their OWN encrypted reasoning, so they are\n // carried independently of any plaintext `reasoning`; see the empty-content\n // branch above.\n const redactedThinkingSpread = collapsed.redactedThinking?.length\n ? { redactedThinking: collapsed.redactedThinking }\n : {};\n const webSearchesSpread = collapsed.webSearches?.length\n ? { webSearches: collapsed.webSearches }\n : {};\n if (collapsed.toolCalls && collapsed.toolCalls.length > 0) {\n const sanitizedToolCalls = collapsed.toolCalls.map((tc) => ({\n ...tc,\n name: tc.name ?? \"\",\n arguments: tc.arguments ?? \"{}\",\n }));\n if (collapsed.content) {\n // Both content and toolCalls present — save as ContentWithToolCallsResponse.\n //\n // Ordered `blocks` (#274) is persisted ONLY when the collapser\n // classified the stream as interleaved — a tool-call delta appeared\n // strictly before the first content delta, OR a content delta\n // appeared after any tool-call delta. The collapser encodes exactly\n // that rule: it sets `collapsed.blocks` only in those cases and\n // leaves it undefined otherwise. So the recorder simply spreads it\n // when present; an ordinary text-then-tools (or text-only) stream has\n // no `blocks` and persists the legacy shape byte-identically.\n const blocksSpread = collapsed.blocks?.length ? { blocks: collapsed.blocks } : {};\n fixtureResponse = {\n content: collapsed.content,\n toolCalls: sanitizedToolCalls,\n ...blocksSpread,\n ...reasoningSpread,\n ...reasoningSignatureSpread,\n ...redactedThinkingSpread,\n ...webSearchesSpread,\n ...usageSpread,\n };\n } else {\n fixtureResponse = {\n toolCalls: sanitizedToolCalls,\n ...reasoningSpread,\n ...reasoningSignatureSpread,\n ...redactedThinkingSpread,\n ...webSearchesSpread,\n ...usageSpread,\n };\n }\n } else {\n fixtureResponse = {\n content: collapsed.content ?? \"\",\n ...reasoningSpread,\n ...reasoningSignatureSpread,\n ...redactedThinkingSpread,\n ...webSearchesSpread,\n ...usageSpread,\n };\n }\n }\n } else {\n // Non-streaming — try to parse as JSON\n let parsedResponse: unknown = null;\n try {\n parsedResponse = JSON.parse(upstreamBody);\n } catch (parseErr) {\n const msg = parseErr instanceof Error ? parseErr.message : \"unknown\";\n defaults.logger.warn(\n `Upstream response is not valid JSON (${msg}) — saving as error fixture`,\n );\n }\n // fal.ai and ElevenLabs Voice Design/create/get/delete return arbitrary JSON shapes\n // (previews with embedded base64 audio, voice objects). Round-trip the\n // payload verbatim instead of letting buildFixtureResponse mis-classify\n // them as ImageResponse / VideoResponse / TranscriptionResponse.\n if (\n (request._endpointType === \"fal\" ||\n request._endpointType === \"elevenlabs-voice-design\" ||\n request._endpointType === \"elevenlabs-voice\" ||\n request._endpointType === \"elevenlabs-voice-get\" ||\n request._endpointType === \"elevenlabs-voice-delete\") &&\n parsedResponse !== null\n ) {\n const obj = parsedResponse as Record<string, unknown>;\n const isErrorShape =\n typeof obj.error === \"object\" &&\n obj.error !== null &&\n typeof (obj.error as Record<string, unknown>).message === \"string\";\n if (isErrorShape) {\n const err = obj.error as Record<string, unknown>;\n fixtureResponse = {\n error: {\n message: String(err.message ?? \"Unknown error\"),\n type: String(err.type ?? \"api_error\"),\n code: err.code ? String(err.code) : undefined,\n },\n status: upstreamStatus,\n };\n } else {\n fixtureResponse = { json: parsedResponse, status: upstreamStatus };\n }\n } else {\n // NOTE: base64 embeddings are decoded unconditionally inside\n // buildFixtureResponse regardless of the request's `encoding_format`, so\n // there is no need to re-parse it here — it was a dead param.\n fixtureResponse = buildFixtureResponse(\n parsedResponse,\n upstreamStatus,\n defaults.logger,\n request,\n );\n }\n }\n\n // Client may have closed its socket before upstream fired `end`.\n // Distinguish two cases based on whether an SSE terminal frame was seen:\n // - sawDone=true: client closed after consuming `data: [DONE]` or a typed\n // `transcript.text.done` event.\n // Upstream ran to completion; the buffered body is intact. Log and\n // proceed to persist the full fixture.\n // - sawDone=false: genuine mid-stream abort. The buffered body is partial;\n // saving it would produce a corrupt fixture. Skip fixture persistence.\n if (clientDisconnected) {\n if (!sawDone) {\n defaults.logger.warn(\n \"Client disconnected mid-stream — skipping fixture save to avoid truncated data\",\n );\n return \"relayed\";\n }\n defaults.logger.warn(\n \"Client closed connection before upstream end — upstream response completed, recording full fixture\",\n );\n }\n\n // Build RecordedTimings from frame timestamps captured during streaming.\n // Requires at least 2 timestamps (first frame + at least one more) to\n // produce meaningful timing data.\n let recordedTimings: RecordedTimings | undefined;\n if (frameTimestamps.length > 1) {\n const ts = frameTimestamps;\n recordedTimings = {\n ttftMs: ts[0] - streamStartTime,\n interChunkDelaysMs: ts.slice(1).map((t, i) => t - ts[i]),\n totalDurationMs: ts[ts.length - 1] - streamStartTime,\n };\n }\n\n const matchRequest = defaults.requestTransform ? defaults.requestTransform(request) : request;\n const metadata = buildFixtureMetadata(request);\n const fixture: Fixture = {\n match: buildFixtureMatch(matchRequest, defaults.record),\n response: fixtureResponse,\n ...(recordedTimings && { recordedTimings }),\n ...(metadata && { metadata }),\n };\n\n const persistWarnings: string[] = [];\n if (collapsed?.truncated) {\n persistWarnings.push(\"Stream response was truncated — fixture may be incomplete\");\n }\n const persistResult = persistFixture({\n record,\n providerKey,\n testId: getTestId(req),\n fixture,\n fixtures,\n warnings: persistWarnings,\n logger: defaults.logger,\n });\n if (persistResult.kind === \"failed\") {\n if (!res.headersSent) {\n res.setHeader(\"X-AIMock-Record-Error\", sanitizeHeaderValue(persistResult.error));\n } else {\n defaults.logger.warn(`Cannot set X-AIMock-Record-Error header — headers already sent`);\n }\n defaults.logger.warn(`Response relayed but NOT saved to disk — see error above`);\n }\n\n // Relay upstream response to client (skip when the response was already\n // streamed progressively by makeUpstreamRequest — headers and body are\n // already on the wire).\n if (streamedToClient) {\n // The hook can't run because the body is already on the wire. Surface\n // the bypass so the caller (typically the chaos layer) can record it —\n // otherwise a configured chaos action silently no-ops on streamed traffic.\n if (options?.beforeWriteResponse && options.onHookBypassed) {\n const bypassReason: \"sse_streamed\" | \"ndjson_streamed\" | \"binary_streamed\" = isBinaryStream\n ? \"binary_streamed\"\n : ctString.toLowerCase().includes(\"application/x-ndjson\")\n ? \"ndjson_streamed\"\n : \"sse_streamed\";\n try {\n options.onHookBypassed(bypassReason);\n } catch (err) {\n defaults.logger.warn(\n `onHookBypassed callback threw: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n } else {\n // Give the caller a chance to mutate or replace the response before relay.\n // Used by the chaos layer to turn a successful proxy into a malformed body.\n // `body` is the raw upstream bytes so binary payloads survive round-tripping.\n if (options?.beforeWriteResponse) {\n let handled: boolean | undefined;\n try {\n handled = await options.beforeWriteResponse({\n status: upstreamStatus,\n contentType: ctString,\n body: rawBuffer,\n });\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(`beforeWriteResponse hook failed for ${providerKey}: ${msg}`);\n }\n if (handled) return \"handled_by_hook\";\n }\n\n // Normalize status codes for the client: aimock acts as a gateway, so\n // upstream provider details (429 rate-limits, 503 outages, etc.) should\n // not leak. Successes → 200, errors → 502 (Bad Gateway).\n const clientStatus = upstreamStatus >= 200 && upstreamStatus < 300 ? 200 : 502;\n const isAudioRelay = ctString.toLowerCase().startsWith(\"audio/\");\n // When an upstream error (non-2xx) is relayed for an audio endpoint, the\n // body is typically a JSON error object — override the content-type so\n // clients don't try to decode JSON as audio.\n const relayHeaders: Record<string, string> = {};\n const clientCt =\n (clientStatus >= 200 && clientStatus < 300) || !isAudioRelay\n ? ctString || \"application/json\"\n : \"application/json\";\n if (clientCt) {\n relayHeaders[\"Content-Type\"] = clientCt;\n }\n res.writeHead(clientStatus, relayHeaders);\n res.end(isBinaryStream || isAudioRelay ? rawBuffer : upstreamBody);\n }\n\n return \"relayed\";\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Decodes a sequence of byte chunks to UTF-8 text for SSE/NDJSON frame\n * splitting on the streamed-capture path. Wraps Node's StringDecoder so a\n * multibyte UTF-8 character (CJK, emoji, ...) whose bytes are split across a\n * TCP chunk boundary buffers across chunks instead of decoding to U+FFFD\n * replacement characters — decoding each chunk independently with\n * Buffer#toString() would corrupt the recorded frame text.\n */\nexport class StreamingFrameDecoder {\n private decoder = new StringDecoder(\"utf8\");\n /** Decode a chunk, holding back any trailing partial multibyte sequence. */\n write(chunk: Buffer): string {\n return this.decoder.write(chunk);\n }\n /** Flush any buffered bytes once the stream has ended. */\n end(): string {\n return this.decoder.end();\n }\n}\n\n/**\n * Sanitize a configured timeout: non-finite or non-positive values fall back\n * to the default. Shared with the OpenRouter-video live lifecycle proxy so\n * `record.upstreamTimeoutMs` is clamped identically on every proxy path.\n */\nexport function clampTimeout(value: number | undefined, fallback: number): number {\n if (value == null || !Number.isFinite(value) || value <= 0) return fallback;\n return value;\n}\n\n/**\n * Sanitize the configured proxy-buffer cap: non-finite or non-positive values\n * fall back to the default. Also hard-clamps to just under V8's max string\n * length so a misconfigured large value can never permit an over-string-limit\n * buffer to reach `.toString()`.\n */\nexport function clampMaxBufferBytes(value: number | undefined): number {\n if (value == null || !Number.isFinite(value) || value <= 0) {\n return DEFAULT_MAX_PROXY_BUFFER_BYTES;\n }\n return Math.min(value, PROXY_BUFFER_HARD_CEILING);\n}\n\n/**\n * Sanitize the configured proxy-buffer frame cap: non-finite or non-positive\n * values fall back to the default. Bounds the count-indexed per-frame state\n * (`frameTimestamps` + parse buffers) that the byte cap cannot bound on its own.\n */\nexport function clampMaxBufferFrames(value: number | undefined): number {\n // INTENTIONAL DIVERGENCE from journal-max's `0 = unbounded` convention: here\n // 0 (and any non-positive / non-finite value) maps to the DEFAULT cap, never\n // \"unbounded\". The frame cap exists as leak-safety for never-ending proxy\n // streams; allowing 0 to disable it would reintroduce the exact unbounded\n // per-frame-state growth this cap guards against. Do NOT make 0 mean\n // unbounded.\n if (value == null || !Number.isFinite(value) || value <= 0) {\n return DEFAULT_MAX_PROXY_BUFFER_FRAMES;\n }\n return Math.floor(value);\n}\n\nfunction makeUpstreamRequest(\n target: URL,\n headers: Record<string, string>,\n body: string,\n clientRes?: http.ServerResponse,\n method: string = \"POST\",\n logger?: Logger,\n timeouts?: Pick<RecordConfig, \"upstreamTimeoutMs\" | \"bodyTimeoutMs\">,\n maxBufferBytes: number = DEFAULT_MAX_PROXY_BUFFER_BYTES,\n maxBufferFrames: number = DEFAULT_MAX_PROXY_BUFFER_FRAMES,\n): Promise<{\n status: number;\n headers: http.IncomingHttpHeaders;\n body: string;\n rawBuffer: Buffer;\n streamedToClient: boolean;\n clientDisconnected: boolean;\n frameTimestamps: number[];\n streamStartTime: number;\n /**\n * True when the upstream response exceeded `maxBufferBytes`. The client still\n * received every byte (the relay is independent of this buffer), but the\n * in-memory buffer was capped, so `body`/`rawBuffer` are partial and the\n * caller MUST skip collapse/recording.\n */\n bufferTruncated: boolean;\n /**\n * Which cap tripped truncation (`\"byte\"` or `\"frame\"`), or `undefined` when\n * not truncated. Lets the caller log accurately which budget was exceeded\n * rather than conflating the two.\n */\n truncationCap?: \"byte\" | \"frame\";\n /** Total bytes seen from upstream (may exceed the buffered amount when capped). */\n totalBytes: number;\n /**\n * True when a NON-progressive (non-teed) upstream body exceeded the proxy\n * hard ceiling, so the retained `rawBuffer` is PARTIAL. The caller MUST fail\n * loud (502) rather than relay this truncated body as a success status.\n */\n hardCeilingExceeded: boolean;\n /**\n * True when a terminal SSE frame was observed in the upstream stream before\n * the client disconnected. The terminal may be legacy `data: [DONE]` or the\n * typed OpenAI transcription event `transcript.text.done`. When\n * `clientDisconnected` is true AND `sawDone` is true, the stream was\n * logically complete, so the fixture SHOULD be persisted. When `sawDone` is\n * false the disconnect was a genuine mid-stream abort and the fixture MUST\n * NOT be persisted.\n */\n sawDone: boolean;\n}> {\n return new Promise((resolve, reject) => {\n const transport = target.protocol === \"https:\" ? https : http;\n const UPSTREAM_TIMEOUT_MS = clampTimeout(timeouts?.upstreamTimeoutMs, 30_000);\n const BODY_TIMEOUT_MS = clampTimeout(timeouts?.bodyTimeoutMs, 30_000);\n // Capture the moment the request is dispatched (set just before\n // `req.write` below). ttft/total durations are measured from request SEND,\n // not from when upstream HEADERS arrive — basing them on headers-received\n // time would exclude the upstream's time-to-first-byte and understate the\n // recorded time-to-first-token.\n let requestSendTime = 0;\n const req = transport.request(\n target,\n {\n method,\n timeout: UPSTREAM_TIMEOUT_MS,\n headers: {\n ...headers,\n \"Content-Length\": Buffer.byteLength(body).toString(),\n },\n },\n (res) => {\n res.setTimeout(BODY_TIMEOUT_MS, () => {\n req.destroy(new Error(`Upstream response timed out after ${BODY_TIMEOUT_MS / 1000}s`));\n });\n // Detect streaming content types so we can tee upstream chunks to the\n // client as they arrive rather than buffering the entire stream and\n // replaying it in a single res.end() at the bottom of proxyAndRecord.\n // Buffering collapses every frame into one client-visible write,\n // which defeats progressive rendering in downstream consumers and\n // can trip HTTP idle timeouts on slow calls.\n const ct = res.headers[\"content-type\"];\n const ctStr = pickContentType(ct);\n const ctLower = ctStr.toLowerCase();\n const isSSE = ctLower.includes(\"text/event-stream\");\n const isNDJSON = ctLower.includes(\"application/x-ndjson\");\n const isBinaryEventStream = ctLower.includes(\"application/vnd.amazon.eventstream\");\n const isProgressiveStream = isSSE || isNDJSON || isBinaryEventStream;\n // SSE/NDJSON frame timing capture — timestamps each complete frame\n // so proxyAndRecord can build RecordedTimings for the fixture.\n const frameTimestamps: number[] = [];\n // Measure from request send (see requestSendTime above), falling back to\n // now only if it was somehow not set before the response callback fired.\n const streamStartTime = requestSendTime || Date.now();\n let frameBuffer = \"\";\n // Decode chunks through a streaming-aware decoder so a multibyte UTF-8\n // character split across a TCP chunk boundary buffers across chunks\n // instead of decoding to U+FFFD replacement characters.\n const frameDecoder = new StreamingFrameDecoder();\n let binaryFrameBuffer = Buffer.alloc(0);\n\n let streamedToClient = false;\n let clientDisconnected = false;\n // A terminal can be legacy `[DONE]` or typed `transcript.text.done`.\n // Either means a client close before upstream `res.end` is complete.\n let sawDone = false;\n if (isProgressiveStream && clientRes && !clientRes.headersSent) {\n const relayHeaders: Record<string, string> = {\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\",\n };\n if (ctStr) relayHeaders[\"Content-Type\"] = ctStr;\n // Normalize status codes for the client: aimock acts as a gateway,\n // so upstream provider details should not leak.\n // Successes → 200, errors → 502 (Bad Gateway).\n const rawStatus = res.statusCode ?? 200;\n const clientStatus = rawStatus >= 200 && rawStatus < 300 ? 200 : 502;\n clientRes.writeHead(clientStatus, relayHeaders);\n // Flush headers immediately so the client starts parsing frames\n // before the first data chunk arrives.\n if (typeof clientRes.flushHeaders === \"function\") clientRes.flushHeaders();\n streamedToClient = true;\n clientRes.on(\"close\", () => {\n if (!clientRes.writableFinished) {\n clientDisconnected = true;\n if (!sawDone) {\n // Genuine mid-stream abort — tear down upstream so it stops\n // producing data, and clear all parse state so no partial\n // fixture is persisted. Mirrors the pre-#288 behavior.\n req.destroy();\n res.removeListener(\"data\", onUpstreamData);\n chunks.length = 0;\n bufferedBytes = 0;\n frameTimestamps.length = 0;\n frameBuffer = \"\";\n binaryFrameBuffer = Buffer.alloc(0);\n }\n // If sawDone is true: client closed after consuming a terminal\n // frame. The upstream is logically complete; let it run to\n // `res.end` so the full body is\n // buffered and the fixture can be persisted. The\n // `!clientDisconnected` guard inside `onUpstreamData` already\n // prevents further writes to the now-closed client socket.\n }\n });\n }\n const chunks: Buffer[] = [];\n // Bound the in-memory buffer used to collapse/journal the response so a\n // single huge proxied stream can neither spike the heap nor build a\n // string past V8's ~512 MiB limit (RangeError: Invalid string length).\n // The client relay below is independent of this buffer, so capping it\n // does NOT truncate what the client receives.\n let bufferedBytes = 0;\n let totalBytes = 0;\n let bufferTruncated = false;\n /** Which cap tripped truncation — surfaced to the caller for an accurate warning. */\n let truncationCap: \"byte\" | \"frame\" | undefined;\n // Snapshot the effective hard ceiling once for this request (honors the\n // test-only override). Bounds the non-progressive relay buffer; when the\n // body would exceed it we can neither buffer the full copy nor safely\n // relay the partial one, so the caller fails loud instead of truncating.\n const hardCeiling = effectiveHardCeiling();\n /**\n * True when a NON-progressive upstream body exceeded `hardCeiling`, so\n * the buffered `rawBuffer` is a PARTIAL copy that must NOT be relayed as\n * success. Distinct from `bufferTruncated` (which also covers the\n * under-ceiling over-soft-cap case where the full body IS retained).\n */\n let hardCeilingExceeded = false;\n // Trip truncation: mark the response over-cap so the caller skips\n // collapse/recording, and eagerly drop the accumulated PARSE state\n // (frameTimestamps + frame/binary parse buffers) which only ever feeds\n // recording. Reused by the top-of-callback byte guard AND the per-frame\n // guards inside the SSE/NDJSON and binary splitter loops, so a single\n // coalesced chunk carrying many complete frames cannot overshoot the\n // frame cap before the next data event re-checks.\n //\n // The raw `chunks` array is handled differently by stream shape:\n // - progressive streams (SSE/NDJSON/binary) are teed to the client\n // live, so the bytes are already on the wire — `chunks` is freed\n // immediately and the partial buffer is never relayed.\n // - non-progressive responses (a single non-stream body) are NOT teed;\n // the only copy the client can receive is `chunks`, so we KEEP\n // accumulating it (bounded by HARD_CEILING below) and relay it in\n // full. We still skip recording — the cap means \"don't journal\", not\n // \"don't answer the client\".\n const tripTruncation = (cap: \"byte\" | \"frame\") => {\n bufferTruncated = true;\n truncationCap = cap;\n frameTimestamps.length = 0;\n frameBuffer = \"\";\n // Intentional: the returned flush string is discarded (frameBuffer was\n // just cleared and recording is skipped), but `.end()` releases any\n // partial-multibyte bytes the StringDecoder is internally holding — so\n // this frees decoder state rather than being dead cleanup.\n frameDecoder.end();\n binaryFrameBuffer = Buffer.alloc(0);\n if (isProgressiveStream) {\n chunks.length = 0;\n bufferedBytes = 0;\n }\n // State which cap tripped accurately (do not conflate the two caps),\n // and report the relay truthfully — the client received every byte on\n // the streamed path and, post-fix, on the non-streamed path too.\n const detail =\n cap === \"byte\"\n ? `byte cap (${maxBufferBytes} bytes)`\n : `frame cap (${maxBufferFrames} frames)`;\n logger?.warn(\n `Upstream response exceeded the proxy buffer ${detail} — relaying full body to client, but skipping in-memory collapse/recording to bound memory`,\n );\n };\n const onUpstreamData = (chunk: Buffer) => {\n totalBytes += chunk.length;\n // Trip truncation on EITHER bytes OR frame count. The byte cap alone\n // never bounds `frameTimestamps` (count-indexed, not byte-sized) nor a\n // never-completing parse buffer, so a long-lived / never-ending stream\n // would otherwise grow per-frame state forever. `frameTimestamps.length`\n // is the running complete-frame count. `>=` (not `>`) so we never\n // retain MORE than `maxBufferFrames` frames — see the \"maximum N\n // frames retained\" contract on DEFAULT_MAX_PROXY_BUFFER_FRAMES.\n if (!bufferTruncated && bufferedBytes + chunk.length > maxBufferBytes) {\n tripTruncation(\"byte\");\n } else if (!bufferTruncated && frameTimestamps.length >= maxBufferFrames) {\n tripTruncation(\"frame\");\n }\n // Buffer the raw bytes. Under cap: always. Over cap on a progressive\n // stream: never (bytes are already teed live; chunks was freed). Over\n // cap on a NON-progressive response: keep buffering for the relay —\n // it has no live tee, so `chunks` is the only copy the client can get\n // — but hard-cap it at HARD_CEILING (well under V8's max string\n // length) so the eventual rawBuffer.toString() relay can never throw\n // RangeError: Invalid string length.\n if (!bufferTruncated) {\n chunks.push(chunk);\n bufferedBytes += chunk.length;\n } else if (!isProgressiveStream) {\n if (bufferedBytes + chunk.length <= hardCeiling) {\n chunks.push(chunk);\n bufferedBytes += chunk.length;\n } else {\n // The non-progressive relay copy would exceed the hard ceiling.\n // We CANNOT relay the full body (it can't be buffered safely) and\n // MUST NOT relay the partial buffer as a success — flag it so the\n // caller fails loud (502) instead of presenting a truncated 2xx.\n hardCeilingExceeded = true;\n }\n }\n\n // Capture per-frame timestamps for SSE/NDJSON streams. Gated on\n // !bufferTruncated so per-frame parse/timing state stops growing once\n // the cap trips (the byte/frame guard above already freed it).\n // TCP data events don't align with SSE frames — buffer and\n // split on the protocol delimiter to timestamp each complete frame.\n if (!bufferTruncated && (isSSE || isNDJSON)) {\n frameBuffer += frameDecoder.write(chunk);\n // Split on the protocol delimiter, tolerating CRLF line endings.\n // The SSE spec permits CRLF, and some upstreams/proxies emit\n // \\r\\n\\r\\n (SSE) or \\r\\n (NDJSON) frame boundaries. An LF-only\n // split would see the whole CRLF stream as a single frame and\n // lose per-frame timing. The last split element (a partial frame\n // tail) stays buffered, exactly as with a string delimiter.\n const delimiter = isNDJSON ? /\\r?\\n/ : /\\r?\\n\\r?\\n/;\n const parts = frameBuffer.split(delimiter);\n // All complete frames (everything except the last part which\n // may be incomplete). Enforce the frame cap PER-FRAME: a single\n // coalesced chunk can carry many complete frames, so checking only\n // at the top of the callback would let one event push them all and\n // overshoot the cap unbounded. Trip + bail mid-loop instead.\n for (let fi = 0; fi < parts.length - 1; fi++) {\n if (frameTimestamps.length >= maxBufferFrames) {\n tripTruncation(\"frame\");\n break;\n }\n const frame = parts[fi].trim();\n if (frame.length > 0) {\n frameTimestamps.push(Date.now());\n // Track typed and legacy terminal frames so the client-close\n // handler can distinguish a complete stream from an abort.\n if (isTerminalSSEFrame(frame)) sawDone = true;\n }\n }\n // Last part stays in buffer (may be incomplete). Skip when the\n // per-frame guard just tripped — tripTruncation already cleared it.\n if (!bufferTruncated) {\n frameBuffer = parts[parts.length - 1];\n }\n }\n\n // Binary EventStream frame boundary detection — parse the 4-byte\n // total-length prefix to detect complete frames without decoding\n // frame contents (CRC validation happens in stream-collapse).\n // Also gated on !bufferTruncated so binaryFrameBuffer stops growing\n // once the cap trips.\n if (!bufferTruncated && isBinaryEventStream) {\n // Count the binary parse buffer's growth toward the byte cap.\n // binaryFrameBuffer is a SECOND, parallel copy of the bytes (the\n // raw `chunks` array also holds them), so without this a\n // never-completing / malformed (`totalLen<12`) frame — which pushes\n // no frameTimestamps — would let a full second copy accumulate up\n // to the byte cap, peaking at ~2× the configured cap. Trip on the\n // COMBINED footprint so the byte cap bounds both copies together.\n // NOTE: `bufferedBytes` ALREADY includes the current `chunk.length`\n // (added in the raw-buffer accumulation above), so the combined\n // footprint is `bufferedBytes + binaryFrameBuffer.length`. Adding\n // `chunk.length` again would double-count it and trip the cap one\n // chunk early (matching the L<byte-guard> accounting at the top).\n if (bufferedBytes + binaryFrameBuffer.length > maxBufferBytes) {\n tripTruncation(\"byte\");\n } else {\n binaryFrameBuffer = Buffer.concat([binaryFrameBuffer, chunk]);\n while (binaryFrameBuffer.length >= 4) {\n if (frameTimestamps.length >= maxBufferFrames) {\n // Per-frame cap: a single chunk can complete many binary\n // frames; trip mid-loop rather than overshoot.\n tripTruncation(\"frame\");\n break;\n }\n const totalLen = binaryFrameBuffer.readUInt32BE(0);\n if (totalLen < 12 || binaryFrameBuffer.length < totalLen) break;\n frameTimestamps.push(Date.now());\n binaryFrameBuffer = binaryFrameBuffer.subarray(totalLen);\n }\n }\n }\n\n if (\n streamedToClient &&\n clientRes &&\n !clientDisconnected &&\n !clientRes.destroyed &&\n !clientRes.writableEnded\n ) {\n try {\n clientRes.write(chunk);\n } catch (writeErr) {\n logger?.debug(\n `Failed to relay chunk to client: ${writeErr instanceof Error ? writeErr.message : \"unknown\"}`,\n );\n clientDisconnected = true;\n }\n }\n };\n res.on(\"data\", onUpstreamData);\n res.on(\"error\", reject);\n res.on(\"end\", () => {\n if (res.socket) res.setTimeout(0);\n // Flush remaining text frame buffer — captures the last frame if\n // the stream ended without a trailing delimiter. Binary EventStream\n // frames are length-prefixed so partial frames at end-of-stream are\n // genuinely incomplete and should not be timestamped. Skipped when\n // truncated: the decoder/parse buffer were already drained+cleared on\n // the trip, and recording is skipped, so there is nothing to flush.\n if (!bufferTruncated && (isSSE || isNDJSON)) {\n // Drain any bytes the decoder buffered for an incomplete multibyte\n // sequence so the final frame text is complete before we test it.\n frameBuffer += frameDecoder.end();\n if (frameBuffer.trim().length > 0) {\n frameTimestamps.push(Date.now());\n }\n }\n const rawBuffer = Buffer.concat(chunks);\n if (\n streamedToClient &&\n clientRes &&\n !clientDisconnected &&\n !clientRes.destroyed &&\n !clientRes.writableEnded\n ) {\n try {\n clientRes.end();\n } catch (endErr) {\n logger?.debug(\n `Failed to end client response: ${endErr instanceof Error ? endErr.message : \"unknown\"}`,\n );\n }\n }\n // Decide the string `body`:\n // - not truncated: stringify the full buffer as usual.\n // - truncated PROGRESSIVE stream: `chunks` was freed on the trip, so\n // rawBuffer is empty; skip toString to keep the path allocation-free\n // (the client already got every byte via the live tee).\n // - truncated NON-progressive response: we deliberately kept the full\n // bytes (bounded by HARD_CEILING) so they can be relayed — stringify\n // them so proxyAndRecord can `res.end(body)` the real response.\n const bodyString =\n !bufferTruncated || (bufferTruncated && !streamedToClient) ? rawBuffer.toString() : \"\";\n resolve({\n status: res.statusCode ?? 500,\n headers: res.headers,\n body: bodyString,\n rawBuffer,\n streamedToClient,\n clientDisconnected,\n frameTimestamps,\n streamStartTime,\n bufferTruncated,\n truncationCap,\n totalBytes,\n hardCeilingExceeded,\n sawDone,\n });\n });\n },\n );\n req.on(\"timeout\", () => {\n req.destroy(\n new Error(\n `Upstream request timed out after ${UPSTREAM_TIMEOUT_MS / 1000}s: ${target.href}`,\n ),\n );\n });\n req.on(\"error\", reject);\n // Stamp the send time immediately before dispatching the body so ttft and\n // total-duration are measured from request send, not headers-received.\n requestSendTime = Date.now();\n req.write(body);\n req.end();\n });\n}\n\n/**\n * A captured Anthropic thinking-block signature is only persisted alongside\n * non-empty plaintext `reasoning` (a bare signature has nothing to attach to on\n * replay). When that gate drops a present signature, warn so the loss is\n * observable (matching the recording-side anomaly convention in this file).\n */\nfunction logDroppedReasoningSignature(\n logger: Logger | undefined,\n reasoning: string | undefined,\n reasoningSignature: string | undefined,\n): void {\n if (reasoningSignature && !reasoning) {\n logger?.warn(\"Dropping captured reasoningSignature — no plaintext reasoning to attach it to\");\n }\n}\n\n/**\n * Coerce a tool call's `arguments` field into the string the fixture contract\n * requires (types.ts `ToolCall.arguments: string`). When the upstream omits the\n * arguments field entirely, `JSON.stringify(undefined)` yields the JS value\n * `undefined` (not a string), which then gets dropped on disk-write and breaks\n * cross-provider replay through the OpenAI streaming path. Coalesce to \"{}\" so\n * `arguments` is ALWAYS a string, matching the streaming recorder's behavior.\n */\nfunction toToolCallArguments(raw: unknown): string {\n if (typeof raw === \"string\") return raw;\n if (raw === undefined || raw === null) return \"{}\";\n return JSON.stringify(raw);\n}\n\n/**\n * `usage` sub-objects that carry numeric breakdowns rather than a scalar. The\n * inner fields are NOT allowlisted here on purpose: the recorder keeps every\n * finite-numeric inner field (documented or forward-compat), matching the\n * load-time validator in fixture-loader.ts, whose index-signature escape hatch\n * accepts undocumented numeric inner keys too. A `Set` (own-key membership via\n * `.has()`, no prototype-chain walk) mirrors that validator's\n * `new Set(Object.keys(...))` classification so anything this recorder writes\n * loads cleanly.\n */\nconst USAGE_OBJECT_FIELDS: ReadonlySet<string> = new Set([\n \"cost_details\",\n \"prompt_tokens_details\",\n \"completion_tokens_details\",\n]);\n\n/**\n * Sanitize a provider-reported `usage` object into the fixture\n * `response.usage` override shape (#368).\n *\n * The recorder persists real upstream usage so replay can serve the provider's\n * ACTUAL token counts (instead of the `ceil(len/4)` estimate) and OpenRouter's\n * `cost` — which previously never survived recording at all. Upstream usage is\n * provider-specific and forward-extending, so this passes fields through rather\n * than whitelisting them, but only in the shapes the fixture contract accepts:\n *\n * - numeric scalars (`prompt_tokens`, `cost`, `native_tokens_prompt`, …) — kept\n * - `is_byok` boolean — kept\n * - the documented breakdown objects — kept, with non-numeric inner fields\n * dropped (the load-time validator rejects those, and a fixture the recorder\n * writes must always load)\n * - anything else (strings, arrays, nulls, unknown objects) — DROPPED, because\n * `fixture-loader.ts` errors on a non-numeric extra `usage` key and a recorded\n * fixture that fails validation is worse than one missing an exotic field\n *\n * Returns `undefined` when nothing survives, so the caller omits `usage`\n * entirely and keeps pre-#368 fixtures byte-identical.\n */\nexport function sanitizeRecordedUsage(raw: unknown): ResponseOverrides[\"usage\"] | undefined {\n if (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) return undefined;\n const out: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(raw as Record<string, unknown>)) {\n if (val === undefined || val === null) continue;\n if (USAGE_OBJECT_FIELDS.has(key)) {\n if (typeof val !== \"object\" || val === null || Array.isArray(val)) continue;\n const inner: Record<string, unknown> = {};\n for (const [innerKey, innerVal] of Object.entries(val as Record<string, unknown>)) {\n // Documented inner fields must be numeric (validator-enforced); undocumented\n // ones are unvalidated, so pass them through only when numeric too — keeping\n // the recorded object uniformly safe to re-load.\n if (typeof innerVal === \"number\" && Number.isFinite(innerVal)) inner[innerKey] = innerVal;\n }\n if (Object.keys(inner).length > 0) out[key] = inner;\n continue;\n }\n if (key === \"is_byok\") {\n if (typeof val === \"boolean\") out[key] = val;\n continue;\n }\n if (typeof val === \"number\" && Number.isFinite(val)) out[key] = val;\n }\n return Object.keys(out).length > 0 ? (out as ResponseOverrides[\"usage\"]) : undefined;\n}\n\n/**\n * Detect the response format from the parsed upstream JSON and convert\n * it into an aimock FixtureResponse.\n */\nfunction buildFixtureResponse(\n parsed: unknown,\n status: number,\n logger?: Logger,\n request?: ChatCompletionRequest,\n): FixtureResponse {\n if (parsed === null || parsed === undefined) {\n // Raw / unparseable response — save as error\n return {\n error: { message: \"Upstream returned non-JSON response\", type: \"proxy_error\" },\n status,\n };\n }\n\n const obj = parsed as Record<string, unknown>;\n\n // Error response — only match the actual { error: { message: \"...\" } } shape\n // used by OpenAI/Anthropic/etc., not arbitrary truthy `.error` fields.\n if (\n typeof obj.error === \"object\" &&\n obj.error !== null &&\n typeof (obj.error as Record<string, unknown>).message === \"string\"\n ) {\n const err = obj.error as Record<string, unknown>;\n return {\n error: {\n message: String(err.message ?? \"Unknown error\"),\n type: String(err.type ?? \"api_error\"),\n code: err.code ? String(err.code) : undefined,\n },\n status,\n };\n }\n\n // OpenAI embeddings: { data: [{ embedding: [...] }] }\n if (Array.isArray(obj.data) && obj.data.length > 0) {\n const first = obj.data[0] as Record<string, unknown>;\n if (Array.isArray(first.embedding)) {\n return { embedding: first.embedding as number[] };\n }\n // A string embedding is a base64-packed Float32 array. Decode it regardless\n // of whether the request echoed `encoding_format: \"base64\"` — some providers\n // return base64 without the client having asked for it, and gating on the\n // request echo silently drops a valid embedding into the error fixture.\n if (typeof first.embedding === \"string\") {\n const buf = Buffer.from(first.embedding, \"base64\");\n if (buf.byteLength === 0 || buf.byteLength % 4 !== 0) {\n // Malformed base64 (not a whole number of Float32s). Don't silently\n // return a valid-looking zero-dimension embedding — log it and fall\n // through to the generic error fixture so the loss is diagnosable.\n logger?.warn(\n `Could not decode base64 embedding (byteLength=${buf.byteLength} is not a positive multiple of 4) — saving as error fixture`,\n );\n } else {\n // Uint8Array constructor copies Buffer data to a fresh ArrayBuffer at offset 0,\n // guaranteeing the alignment Float32Array requires.\n const copied = new Uint8Array(buf);\n const floats = new Float32Array(copied.buffer, 0, buf.byteLength / 4);\n return { embedding: Array.from(floats) };\n }\n }\n // OpenAI image generation: { created, data: [{ url, b64_json, revised_prompt }] }\n // Enter the branch when ANY item carries media — not just data[0]. A batch\n // whose first element lacks both url and b64_json (e.g. a partial/placeholder\n // entry) but whose later element HAS one would otherwise skip the branch and\n // fall through to the error fixture, silently dropping every captured image.\n if ((obj.data as Array<Record<string, unknown>>).some((item) => item.url || item.b64_json)) {\n // Map only items that actually carry media (url or b64_json). A later item\n // lacking both — including one whose `b64_json` is an empty string — would\n // otherwise produce an empty {} image entry (all the conditional spreads\n // are skipped) — silent fidelity loss.\n const dataItems = obj.data as Array<Record<string, unknown>>;\n const images = dataItems\n .filter((item) => item.url || item.b64_json)\n .map((item) => ({\n ...(item.url ? { url: String(item.url) } : {}),\n ...(item.b64_json ? { b64Json: String(item.b64_json) } : {}),\n ...(item.revised_prompt ? { revisedPrompt: String(item.revised_prompt) } : {}),\n }));\n // Surface any dropped items (e.g. empty-string b64_json placeholders) so\n // the fidelity loss is diagnosable rather than silent.\n const droppedCount = dataItems.length - images.length;\n if (droppedCount > 0) {\n logger?.warn(\n `Dropped ${droppedCount} image item(s) from batch lacking a non-empty url or b64_json`,\n );\n }\n if (images.length === 1) {\n return { image: images[0] };\n }\n return { images };\n }\n }\n\n // Gemini Imagen: { predictions: [...] }\n if (Array.isArray(obj.predictions)) {\n const images = (obj.predictions as Array<Record<string, unknown>>).map((p) => ({\n ...(p.bytesBase64Encoded ? { b64Json: String(p.bytesBase64Encoded) } : {}),\n ...(p.mimeType ? { mimeType: String(p.mimeType) } : {}),\n }));\n if (images.length === 1) {\n return { image: images[0] };\n }\n return { images };\n }\n\n // OpenAI transcription: { text: \"...\", ... }. Modern gpt-transcribe\n // responses may be the minimal { text, languages?, usage? } shape, so trust\n // that shape only on an audio transcription/translation request. Other\n // endpoints still need the legacy markers to avoid misclassifying text APIs.\n const isTranscriptionRequest =\n request?._endpointType === \"transcription\" || request?._endpointType === \"translation\";\n const looksLikeTranscription =\n typeof obj.text === \"string\" &&\n (isTranscriptionRequest ||\n obj.task === \"transcribe\" ||\n (obj.language !== undefined && obj.duration !== undefined)) &&\n !(\"choices\" in obj) &&\n !(\"candidates\" in obj) &&\n !(\"object\" in obj) &&\n !(\"message\" in obj) &&\n !(\"outputs\" in obj);\n if (looksLikeTranscription) {\n return {\n transcription: {\n text: obj.text as string,\n ...(obj.language ? { language: String(obj.language) } : {}),\n ...(Array.isArray(obj.languages)\n ? {\n languages: obj.languages\n .filter(\n (language): language is Record<string, unknown> =>\n typeof language === \"object\" &&\n language !== null &&\n typeof language.code === \"string\",\n )\n .map((language) => ({ code: language.code as string })),\n }\n : {}),\n ...(obj.duration !== undefined ? { duration: Number(obj.duration) } : {}),\n ...(obj.usage && typeof obj.usage === \"object\"\n ? { usage: obj.usage as Record<string, unknown> }\n : {}),\n ...(Array.isArray(obj.words) ? { words: obj.words } : {}),\n ...(Array.isArray(obj.segments) ? { segments: obj.segments } : {}),\n },\n };\n }\n\n // Gemini Interactions: { id, status, outputs: [{ type: \"text\", text }, { type: \"function_call\", name, arguments }] }\n if (\n Array.isArray(obj.outputs) &&\n obj.outputs.length > 0 &&\n !(\"choices\" in obj) &&\n !(\"content\" in obj) &&\n !(\"candidates\" in obj)\n ) {\n const outputs = obj.outputs as Array<Record<string, unknown>>;\n const fnCallOutputs = outputs.filter((o) => o.type === \"function_call\");\n const textOutputs = outputs.filter((o) => o.type === \"text\" && typeof o.text === \"string\");\n const hasToolCalls = fnCallOutputs.length > 0;\n const joinedText = textOutputs.map((o) => String(o.text ?? \"\")).join(\"\");\n const hasContent = joinedText.length > 0;\n\n if (hasToolCalls) {\n const toolCalls: ToolCall[] = fnCallOutputs.map((o) => ({\n name: String(o.name),\n arguments: toToolCallArguments(o.arguments),\n ...(o.id ? { id: String(o.id) } : {}),\n }));\n if (hasContent) {\n return { content: joinedText, toolCalls };\n }\n return { toolCalls };\n }\n if (hasContent) {\n return { content: joinedText };\n }\n // Recognized Gemini Interactions shape but empty content\n return { content: \"\" };\n }\n\n // OpenAI video generation: { id, status, ... }\n // Guard against false positives: many API responses have `id` + `status` fields\n // (e.g. chat completions, Anthropic messages). Reject if the response has fields\n // that indicate a known non-video format.\n if (\n typeof obj.id === \"string\" &&\n typeof obj.status === \"string\" &&\n (obj.status === \"completed\" || obj.status === \"in_progress\" || obj.status === \"failed\") &&\n !(\"choices\" in obj) &&\n !(\"content\" in obj) &&\n !(\"candidates\" in obj) &&\n !(\"message\" in obj) &&\n !(\"data\" in obj) &&\n !(\"object\" in obj) &&\n !(\"outputs\" in obj) &&\n !(\"model\" in obj) &&\n !(\"response\" in obj) &&\n !(\"done\" in obj) &&\n !(\"usage\" in obj) &&\n !(\"error\" in obj)\n ) {\n if (obj.status === \"completed\" && obj.url) {\n return {\n video: {\n id: String(obj.id),\n status: \"completed\" as const,\n url: String(obj.url),\n },\n };\n }\n return {\n video: {\n id: String(obj.id),\n status: obj.status === \"failed\" ? (\"failed\" as const) : (\"processing\" as const),\n },\n };\n }\n\n // Direct embedding: { embedding: [...] }\n if (Array.isArray(obj.embedding)) {\n return { embedding: obj.embedding as number[] };\n }\n\n // OpenAI chat completion: { choices: [{ message: { content, tool_calls } }] }\n if (Array.isArray(obj.choices) && obj.choices.length > 0) {\n const choice = obj.choices[0] as Record<string, unknown>;\n const message = choice.message as Record<string, unknown> | undefined;\n if (message) {\n const hasToolCalls = Array.isArray(message.tool_calls) && message.tool_calls.length > 0;\n const hasContent = typeof message.content === \"string\" && message.content.length > 0;\n\n // Reasoning is exposed under different keys across OpenAI-compatible\n // providers: OpenAI/vLLM use `reasoning_content`, while DeepSeek and\n // OpenRouter use `reasoning`. Read both (preferring `reasoning_content`)\n // so a reasoning turn is not silently dropped on the latter providers.\n const openaiReasoning =\n typeof message.reasoning_content === \"string\" && message.reasoning_content.length > 0\n ? message.reasoning_content\n : typeof message.reasoning === \"string\" && message.reasoning.length > 0\n ? message.reasoning\n : undefined;\n\n // Provider-reported usage from the non-streaming envelope (#368), at\n // parity with the collapsed-stream path (both capture usage for\n // OpenAI-compatible providers only — only `collapseOpenAISSE` sets\n // `CollapseResult.usage`): replay then serves the real token counts rather\n // than the `ceil(len/4)` estimate, and OpenRouter's `usage.cost` survives\n // recording. Omitted when upstream sent no usage.\n const openaiUsage = sanitizeRecordedUsage(obj.usage);\n const usageSpread = openaiUsage ? { usage: openaiUsage } : {};\n\n if (hasToolCalls) {\n const toolCalls: ToolCall[] = (message.tool_calls as Array<Record<string, unknown>>).map(\n (tc) => {\n const fn = tc.function as Record<string, unknown>;\n return {\n name: String(fn.name),\n arguments: toToolCallArguments(fn.arguments),\n ...(tc.id ? { id: String(tc.id) } : {}),\n };\n },\n );\n if (hasContent) {\n return {\n content: message.content as string,\n toolCalls,\n ...(openaiReasoning ? { reasoning: openaiReasoning } : {}),\n ...usageSpread,\n };\n }\n return {\n toolCalls,\n ...(openaiReasoning ? { reasoning: openaiReasoning } : {}),\n ...usageSpread,\n };\n }\n // Text content only\n if (hasContent) {\n return {\n content: message.content as string,\n ...(openaiReasoning ? { reasoning: openaiReasoning } : {}),\n ...usageSpread,\n };\n }\n // Recognized OpenAI shape but empty content (e.g. content filtering, zero max_tokens)\n return {\n content: \"\",\n ...(openaiReasoning ? { reasoning: openaiReasoning } : {}),\n ...usageSpread,\n };\n }\n }\n\n // Anthropic: { content: [{ type: \"text\", text: \"...\" }] } or tool_use\n if (Array.isArray(obj.content) && obj.content.length > 0) {\n const blocks = obj.content as Array<Record<string, unknown>>;\n const toolUseBlocks = blocks.filter((b) => b.type === \"tool_use\");\n const textBlocks = blocks.filter((b) => b.type === \"text\" && typeof b.text === \"string\");\n const thinkingBlocks = blocks.filter((b) => b.type === \"thinking\");\n // Raw `redacted_thinking` block presence drives reasoning-bearing\n // classification below (mirrors how `thinkingBlocks` keys on PRESENCE, not\n // surviving payload). A turn whose redacted blocks all carry empty `data`\n // is constructible upstream and must NOT fall through to the error fallback.\n const redactedBlocks = blocks.filter((b) => b.type === \"redacted_thinking\");\n // A `redacted_thinking` block carries its encrypted reasoning in an opaque\n // `data` string; collect the SURVIVING (non-empty) payloads in content-array\n // order for the persisted fixture so the recorded turn round-trips its\n // redacted blocks (mirrors the streaming collapse path; see\n // capturedRedactedData for the non-empty rule — empty-data blocks are dropped\n // from the payload, but still count toward classification via redactedBlocks).\n const redactedThinking = blocks\n .map((b) => capturedRedactedData(b))\n .filter((data): data is string => data !== undefined);\n const hasToolCalls = toolUseBlocks.length > 0;\n const joinedText = textBlocks.map((b) => String(b.text ?? \"\")).join(\"\");\n const hasContent = joinedText.length > 0;\n const anthropicReasoning =\n thinkingBlocks.length > 0\n ? thinkingBlocks.map((b) => String(b.thinking ?? \"\")).join(\"\")\n : undefined;\n // The real cryptographic signature is carried only when reasoning is also\n // present (a bare signature has nothing to attach to on replay), matching the\n // streaming recorder's gating. Multi-thinking-block parity: collapseAnthropicSSE\n // overwrites reasoningSignature on every signature_delta (last-signature-wins),\n // and a thinking block that streams NO signature_delta leaves the prior value\n // intact. Mirror both: take the LAST block that actually carries a signature, so\n // a block missing one does not clobber an earlier signature.\n const anthropicReasoningSignature = (() => {\n let sig: string | undefined;\n for (const b of thinkingBlocks) {\n if (typeof b.signature === \"string\") sig = String(b.signature);\n }\n return sig;\n })();\n // Carry the real Anthropic thinking-block signature only when reasoning is\n // also present; redacted blocks carry their OWN encrypted reasoning so they\n // are carried independently of any plaintext `reasoning`. Both mirror the\n // streaming spread gating in proxyAndRecord.\n const reasoningSignatureSpread =\n anthropicReasoning && anthropicReasoningSignature\n ? { reasoningSignature: anthropicReasoningSignature }\n : {};\n logDroppedReasoningSignature(logger, anthropicReasoning, anthropicReasoningSignature);\n const redactedThinkingSpread = redactedThinking.length > 0 ? { redactedThinking } : {};\n\n if (hasToolCalls) {\n const toolCalls: ToolCall[] = toolUseBlocks.map((b) => ({\n name: String(b.name),\n arguments: toToolCallArguments(b.input),\n ...(b.id ? { id: String(b.id) } : {}),\n }));\n if (hasContent) {\n return {\n content: joinedText,\n toolCalls,\n ...(anthropicReasoning ? { reasoning: anthropicReasoning } : {}),\n ...reasoningSignatureSpread,\n ...redactedThinkingSpread,\n };\n }\n return {\n toolCalls,\n ...(anthropicReasoning ? { reasoning: anthropicReasoning } : {}),\n ...reasoningSignatureSpread,\n ...redactedThinkingSpread,\n };\n }\n if (hasContent) {\n return {\n content: joinedText,\n ...(anthropicReasoning ? { reasoning: anthropicReasoning } : {}),\n ...reasoningSignatureSpread,\n ...redactedThinkingSpread,\n };\n }\n // Thinking-only / redacted-only response (no text, no tool calls). A turn can\n // carry only thinking blocks (even ones whose plaintext is empty but which\n // bear a real signature) or only redacted_thinking blocks (even ones whose\n // `data` is empty and thus dropped from the persisted payload), so key on the\n // PRESENCE of those RAW blocks — not the truthiness of the joined thinking text\n // nor the post-filter `redactedThinking` array — and produce a normal\n // empty-content fixture rather than falling through to the error fallback\n // below. This matches the streaming path, which classifies on content\n // emptiness for the same logical turn. (Per the persistence contract, a bare\n // signature with empty reasoning is still dropped via\n // logDroppedReasoningSignature, and empty-data redacted blocks yield NO\n // redactedThinking field.)\n if (thinkingBlocks.length > 0 || redactedBlocks.length > 0) {\n return {\n content: \"\",\n ...(anthropicReasoning ? { reasoning: anthropicReasoning } : {}),\n ...reasoningSignatureSpread,\n ...redactedThinkingSpread,\n };\n }\n }\n\n // Gemini: { candidates: [{ content: { parts: [{ text: \"...\" }] } }] }\n if (Array.isArray(obj.candidates) && obj.candidates.length > 0) {\n const candidate = obj.candidates[0] as Record<string, unknown>;\n const content = candidate.content as Record<string, unknown> | undefined;\n if (content && Array.isArray(content.parts)) {\n const parts = content.parts as Array<Record<string, unknown>>;\n\n // Audio inlineData parts take priority over text. Key on the PRESENCE of\n // `inlineData.data` rather than solely on the mimeType: an audio part can\n // arrive with a missing or unexpected mimeType, and filtering strictly on\n // an `audio/` prefix would then drop a part that genuinely carries audio\n // bytes, producing an empty b64Json. A NON-audio mimeType (e.g. `image/`)\n // still routes elsewhere, so image inlineData is not misclassified — only\n // an explicit `audio/` prefix OR a missing/blank mimeType counts as audio.\n const audioParts = parts.filter((p: Record<string, unknown>) => {\n const inline = p.inlineData as Record<string, unknown> | undefined;\n if (\n inline === undefined ||\n inline === null ||\n typeof inline.data !== \"string\" ||\n inline.data.length === 0\n ) {\n return false;\n }\n const mt = inline.mimeType;\n if (typeof mt === \"string\" && mt.length > 0) {\n return mt.startsWith(\"audio/\");\n }\n // Missing/blank mimeType but data present → treat as audio.\n return true;\n });\n if (audioParts.length > 0) {\n const inlineData = audioParts[0].inlineData as Record<string, unknown>;\n return {\n audio: {\n b64Json: String(inlineData.data ?? \"\"),\n contentType:\n typeof inlineData.mimeType === \"string\" && inlineData.mimeType.length > 0\n ? inlineData.mimeType\n : \"audio/mpeg\",\n },\n };\n }\n\n const fnCallParts = parts.filter((p) => p.functionCall);\n const textParts = parts.filter((p) => typeof p.text === \"string\" && !p.thought);\n const thoughtParts = parts.filter((p) => p.thought === true && typeof p.text === \"string\");\n const hasToolCalls = fnCallParts.length > 0;\n const joinedText = textParts.map((p) => String(p.text ?? \"\")).join(\"\");\n const hasContent = joinedText.length > 0;\n const geminiReasoning =\n thoughtParts.length > 0\n ? thoughtParts.map((p) => String(p.text ?? \"\")).join(\"\")\n : undefined;\n\n if (hasToolCalls) {\n const toolCalls: ToolCall[] = fnCallParts.map((p) => {\n const fc = p.functionCall as Record<string, unknown>;\n return {\n name: String(fc.name),\n arguments: toToolCallArguments(fc.args),\n };\n });\n if (hasContent) {\n return {\n content: joinedText,\n toolCalls,\n ...(geminiReasoning ? { reasoning: geminiReasoning } : {}),\n };\n }\n return { toolCalls, ...(geminiReasoning ? { reasoning: geminiReasoning } : {}) };\n }\n if (hasContent) {\n return {\n content: joinedText,\n ...(geminiReasoning ? { reasoning: geminiReasoning } : {}),\n };\n }\n // Recognized Gemini shape but empty content\n return { content: \"\", ...(geminiReasoning ? { reasoning: geminiReasoning } : {}) };\n }\n }\n\n // Bedrock Converse: { output: { message: { role, content: [{ text }, { toolUse }] } } }\n if (obj.output && typeof obj.output === \"object\") {\n const output = obj.output as Record<string, unknown>;\n const msg = output.message as Record<string, unknown> | undefined;\n if (msg && Array.isArray(msg.content)) {\n const blocks = msg.content as Array<Record<string, unknown>>;\n const toolUseBlocks = blocks.filter((b) => b.toolUse);\n const textBlocks = blocks.filter((b) => typeof b.text === \"string\");\n const reasoningBlocks = blocks.filter((b) => b.reasoningContent);\n const hasToolCalls = toolUseBlocks.length > 0;\n const joinedText = textBlocks.map((b) => String(b.text ?? \"\")).join(\"\");\n const hasContent = joinedText.length > 0;\n const bedrockReasoning =\n reasoningBlocks.length > 0\n ? reasoningBlocks\n .map((b) => {\n const rc = b.reasoningContent as Record<string, unknown>;\n const rt = rc?.reasoningText as Record<string, unknown> | undefined;\n return String(rt?.text ?? \"\");\n })\n .join(\"\")\n : undefined;\n\n if (hasToolCalls) {\n const toolCalls: ToolCall[] = toolUseBlocks.map((b) => {\n const tu = b.toolUse as Record<string, unknown>;\n return {\n name: String(tu.name ?? \"\"),\n arguments: toToolCallArguments(tu.input),\n ...(tu.toolUseId ? { id: String(tu.toolUseId) } : {}),\n };\n });\n if (hasContent) {\n return {\n content: joinedText,\n toolCalls,\n ...(bedrockReasoning ? { reasoning: bedrockReasoning } : {}),\n };\n }\n return { toolCalls, ...(bedrockReasoning ? { reasoning: bedrockReasoning } : {}) };\n }\n if (hasContent) {\n return {\n content: joinedText,\n ...(bedrockReasoning ? { reasoning: bedrockReasoning } : {}),\n };\n }\n // Recognized Bedrock Converse shape but empty content\n return { content: \"\", ...(bedrockReasoning ? { reasoning: bedrockReasoning } : {}) };\n }\n }\n\n // Cohere v2 chat: { finish_reason: \"...\", message: { content: [{ type: \"text\", text: \"...\" }] } }\n // Must come before Ollama since both have `message`, but Cohere has `finish_reason` at top level\n // (not nested in `choices`) and `message.content` as an array of typed objects.\n if (\n typeof obj.finish_reason === \"string\" &&\n obj.message &&\n typeof obj.message === \"object\" &&\n Array.isArray((obj.message as Record<string, unknown>).content)\n ) {\n const msg = obj.message as Record<string, unknown>;\n const contentBlocks = msg.content as Array<Record<string, unknown>>;\n // Join ALL text blocks, not just the first — a multi-block Cohere response\n // would otherwise be truncated to its leading segment.\n const joinedText = contentBlocks\n .filter((b) => b.type === \"text\" && typeof b.text === \"string\")\n .map((b) => String(b.text ?? \"\"))\n .join(\"\");\n const hasContent = joinedText.length > 0;\n const toolCallBlocks = contentBlocks.filter((b) => b.type === \"tool_call\");\n\n // Also check message-level tool_calls (Cohere v2 puts tool calls here, not in content blocks)\n const msgToolCalls = Array.isArray(msg.tool_calls)\n ? (msg.tool_calls as Array<Record<string, unknown>>)\n : [];\n\n if (toolCallBlocks.length > 0) {\n const toolCalls: ToolCall[] = toolCallBlocks.map((b) => ({\n name: String(b.name ?? (b.function as Record<string, unknown>)?.name ?? \"\"),\n arguments:\n typeof b.parameters === \"string\"\n ? b.parameters\n : typeof b.parameters === \"object\"\n ? JSON.stringify(b.parameters)\n : typeof (b.function as Record<string, unknown>)?.arguments === \"string\"\n ? String((b.function as Record<string, unknown>).arguments)\n : toToolCallArguments((b.function as Record<string, unknown>)?.arguments),\n ...(b.id ? { id: String(b.id) } : {}),\n }));\n if (hasContent) {\n return { content: joinedText, toolCalls };\n }\n return { toolCalls };\n }\n if (msgToolCalls.length > 0) {\n const toolCalls: ToolCall[] = msgToolCalls.map((tc) => {\n const fn = tc.function as Record<string, unknown> | undefined;\n return {\n name: String(tc.name ?? fn?.name ?? \"\"),\n arguments:\n typeof tc.parameters === \"string\"\n ? tc.parameters\n : typeof tc.parameters === \"object\"\n ? JSON.stringify(tc.parameters)\n : typeof fn?.arguments === \"string\"\n ? String(fn.arguments)\n : toToolCallArguments(fn?.arguments),\n ...(tc.id ? { id: String(tc.id) } : {}),\n };\n });\n if (hasContent) {\n return { content: joinedText, toolCalls };\n }\n return { toolCalls };\n }\n if (hasContent) {\n return { content: joinedText };\n }\n // Recognized Cohere v2 shape (finish_reason + message.content array) with\n // no text and no tool calls. Own the empty turn here and return an\n // empty-content fixture rather than falling through to the Ollama branch\n // below, which would re-handle `message.content` and mis-route it.\n return { content: \"\" };\n }\n\n // Ollama: { message: { content: \"...\", tool_calls: [...] } }\n if (obj.message && typeof obj.message === \"object\") {\n const msg = obj.message as Record<string, unknown>;\n const hasOllamaToolCalls = Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0;\n const hasOllamaContent = typeof msg.content === \"string\" && msg.content.length > 0;\n\n if (hasOllamaToolCalls) {\n const toolCalls: ToolCall[] = (msg.tool_calls as Array<Record<string, unknown>>)\n .filter((tc) => tc.function != null)\n .map((tc) => {\n const fn = tc.function as Record<string, unknown>;\n return {\n name: String(fn.name ?? \"\"),\n arguments: toToolCallArguments(fn.arguments),\n };\n });\n if (hasOllamaContent) {\n return { content: msg.content as string, toolCalls };\n }\n return { toolCalls };\n }\n if (hasOllamaContent) {\n return { content: msg.content as string };\n }\n // Ollama message with content array (like Cohere)\n if (Array.isArray(msg.content) && msg.content.length > 0) {\n const first = msg.content[0] as Record<string, unknown>;\n if (typeof first.text === \"string\") {\n return { content: first.text };\n }\n }\n }\n\n // Ollama /api/generate: { response: \"...\", done: true/false }\n // Narrowed: require `done` to be a boolean — Ollama always sends a boolean\n // here, and gating merely on `\"done\" in obj` would capture unrelated payloads\n // that happen to carry a `response` string and some other `done`-keyed value.\n if (typeof obj.response === \"string\" && typeof obj.done === \"boolean\") {\n return { content: obj.response };\n }\n\n // Fallback: unknown format — save as error. Log the observed top-level shape\n // so an unrecognized/new provider response is diagnosable instead of silently\n // becoming an opaque error fixture.\n logger?.warn(\n `Could not detect response format from upstream (status=${status}) — saving as error fixture; top-level keys: [${Object.keys(\n obj,\n ).join(\", \")}]`,\n );\n return {\n error: {\n message: \"Could not detect response format from upstream\",\n type: \"proxy_error\",\n },\n status,\n };\n}\n\n/**\n * Derive fixture match criteria from the original request.\n */\ntype EndpointType = NonNullable<FixtureMatch[\"endpoint\"]>;\n\nexport function buildFixtureMatch(\n request: ChatCompletionRequest,\n recordConfig?: RecordConfig,\n): {\n userMessage?: string;\n inputText?: string;\n model?: string;\n endpoint?: EndpointType;\n turnIndex?: number;\n hasToolResult?: boolean;\n context?: string;\n} {\n const match: {\n userMessage?: string;\n inputText?: string;\n model?: string;\n endpoint?: EndpointType;\n turnIndex?: number;\n hasToolResult?: boolean;\n context?: string;\n } = {};\n\n // Include endpoint type for multimedia fixtures\n if (request._endpointType && request._endpointType !== \"chat\") {\n match.endpoint = request._endpointType as EndpointType;\n }\n\n // Embedding request\n if (request.embeddingInput) {\n match.inputText = request.embeddingInput;\n return match;\n }\n\n // Chat/multimedia request — match on the last user message\n const lastUser = getLastMessageByRole(request.messages ?? [], \"user\");\n if (lastUser) {\n const text = getTextContent(lastUser.content);\n if (text) {\n match.userMessage = text;\n }\n }\n\n // Record normalized model for all requests so fixtures disambiguate\n // calls that share the same userMessage but target different models.\n if (request.model) {\n match.model =\n normalizeModelName(request.model, recordConfig?.recordFullModelVersion) ?? request.model;\n }\n\n // Multi-turn disambiguation: writing only `userMessage` lets the recorder's\n // in-memory cache shadow follow-up turns that share it (initial tool call\n // vs. text reply after the tool result). turnIndex + hasToolResult give\n // each call a distinct, matcher-aware key. Skip for non-chat (no messages).\n //\n // hasToolResult MUST be stamped with the SAME current-turn predicate the\n // matcher uses ({@link currentTurnHasToolResult}) — NOT a whole-conversation\n // `messages.some(role === \"tool\")`. A whole-conversation stamp on a genuine\n // turn-2 leg-1 request (a fresh user question whose history still carries an\n // earlier turn's tool result) writes `true`, but the matcher computes the\n // turn-scoped `false` for that same request, so the fixture could never match\n // its own recorded request. Sharing the one helper is what keeps record and\n // replay symmetric.\n const messages = request.messages ?? [];\n if (\n messages.length > 0 &&\n (request._endpointType === \"chat\" || request._endpointType === undefined)\n ) {\n match.turnIndex = messages.filter((m) => m.role === \"assistant\").length;\n match.hasToolResult = currentTurnHasToolResult(messages);\n }\n\n if (request._context) {\n match.context = request._context;\n }\n\n return match;\n}\n\n/**\n * Build optional metadata for drift detection. Contains 8-char SHA-256\n * hashes of the system prompt and tool definitions present in the request.\n * Returns undefined when neither is present.\n */\nfunction buildFixtureMetadata(\n request: ChatCompletionRequest,\n): { systemHash?: string; toolsHash?: string } | undefined {\n const meta: { systemHash?: string; toolsHash?: string } = {};\n\n const messages = request.messages ?? [];\n const systemTexts = messages\n .filter((m) => m.role === \"system\")\n .map((m) => (typeof m.content === \"string\" ? m.content : JSON.stringify(m.content)))\n .join(\"\\n\");\n if (systemTexts) {\n meta.systemHash = crypto.createHash(\"sha256\").update(systemTexts).digest(\"hex\").slice(0, 8);\n }\n\n if (request.tools && request.tools.length > 0) {\n meta.toolsHash = crypto\n .createHash(\"sha256\")\n .update(JSON.stringify(request.tools))\n .digest(\"hex\")\n .slice(0, 8);\n }\n\n return Object.keys(meta).length > 0 ? meta : undefined;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"recorder.js","names":["crypto","http"],"sources":["../src/recorder.ts"],"sourcesContent":["import * as http from \"node:http\";\nimport * as https from \"node:https\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as crypto from \"node:crypto\";\nimport { StringDecoder } from \"node:string_decoder\";\nimport type {\n ChatCompletionRequest,\n Fixture,\n FixtureMatch,\n FixtureResponse,\n ResponsesToolsMode,\n RecordConfig,\n RecordedTimings,\n RecordProviderKey,\n ResponseOverrides,\n ToolCall,\n} from \"./types.js\";\nimport { getLastMessageByRole, getTextContent, currentTurnHasToolResult } from \"./router.js\";\nimport { normalizeModelName } from \"./model-utils.js\";\nimport type { Logger } from \"./logger.js\";\nimport {\n collapseStreamingResponseWithResponsesTools,\n capturedRedactedData,\n withoutResponsesToolFields,\n} from \"./stream-collapse.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport { resolveUpstreamUrl } from \"./url.js\";\nimport { applyConfiguredProviderAuth, applyProviderAuth } from \"./provider-auth.js\";\nimport { isAuthenticatedRequest, isRecognizedApiKeyHeader } from \"./api-key-auth.js\";\nimport { getTestId, slugifyTestId, slugifyContext, hasMintedRequestId } from \"./helpers.js\";\nimport { DEFAULT_TEST_ID } from \"./constants.js\";\n\n/** True when an SSE frame completes an OpenAI stream. */\nfunction isTerminalSSEFrame(frame: string): boolean {\n const data = frame\n .split(/\\r?\\n/)\n .filter((line) => line.startsWith(\"data:\"))\n .map((line) => line.slice(\"data:\".length).trimStart())\n .join(\"\\n\")\n .trim();\n\n if (data === \"[DONE]\") return true;\n\n try {\n return (JSON.parse(data) as { type?: unknown }).type === \"transcript.text.done\";\n } catch {\n return false;\n }\n}\n\n/** Headers to strip when proxying — hop-by-hop (RFC 2616 §13.5.1) + client-set. */\n/**\n * Default ceiling (bytes) for the in-memory proxy-path buffer. Chosen well\n * under V8's ~512 MiB max string length so `rawBuffer.toString()` cannot throw\n * `RangeError: Invalid string length`, and so a single huge proxied response\n * cannot spike the heap unbounded. Overridable via\n * `RecordConfig.maxProxyBufferBytes` / `--max-proxy-buffer-bytes`.\n *\n * NOTE: this byte cap bounds the RAW BUFFER only. `stream-collapse` accumulates\n * its own per-channel strings (`content`/`reasoning`/tool `arguments`/etc.) from\n * that buffer and is ALSO reachable directly (exported from index.ts), so it\n * carries its OWN independent guard (`MAX_COLLAPSE_STRING_LENGTH`) rather than\n * relying on this cap — an earlier version of this comment wrongly asserted\n * stream-collapse \"never throws Invalid string length\", which was false for the\n * unbounded `content += delta` accumulators (the ~1/sec prod RangeError).\n */\nexport const DEFAULT_MAX_PROXY_BUFFER_BYTES = 64 * 1024 * 1024; // 64 MiB\n\n/**\n * Default ceiling for the number of SSE/NDJSON/EventStream frames whose\n * per-frame state (`frameTimestamps`, parse buffers) aimock retains for a\n * single proxied response. Frame state is count-indexed, not byte-sized, so a\n * long-lived / never-ending stream accumulates `frameTimestamps` entries (and,\n * if a frame never completes, parse-buffer bytes) UNBOUNDED even when the byte\n * cap is generous — observed as multi-GB heap growth over many hours from a few\n * long nested-sub-agent streams. Tripping truncation on EITHER bytes OR frame\n * count bounds both. 5M frames is generous for any real response (a normal\n * completion is hundreds-to-thousands of frames) while still bounding a runaway\n * stream to ~tens of MB of frame state. Overridable via\n * `RecordConfig.maxProxyBufferFrames` / `--max-proxy-buffer-frames`.\n */\nexport const DEFAULT_MAX_PROXY_BUFFER_FRAMES = 5_000_000;\n\n/**\n * Absolute hard ceiling (bytes) for any in-memory proxy buffer, independent of\n * the configurable `maxProxyBufferBytes`. V8's maximum STRING length on 64-bit\n * is 2^29 - 1 (~512 MiB of UTF-16 code units), and the proxy buffer is\n * eventually stringified via `rawBuffer.toString()` for collapse/relay — so the\n * BYTE buffer must stay safely under that string limit or the toString throws\n * `RangeError: Invalid string length`. 256 MiB of bytes is well under the\n * ~512 MiB string-length boundary (these are different units — bytes vs UTF-16\n * code units — but 256 MiB of bytes can never decode to more than 256 Mi code\n * units, comfortably below 2^29 - 1). Used to clamp the configurable cap AND to\n * bound the non-progressive relay buffer that must be retained past a cap trip.\n */\nexport const PROXY_BUFFER_HARD_CEILING = 256 * 1024 * 1024; // 256 MiB\n\n/**\n * Test-only override of the effective hard ceiling. Lets the proxy-buffer\n * enforcement suite exercise the >hard-ceiling fail-loud path with a small\n * body instead of streaming 256 MiB. `undefined` (the default) uses the real\n * `PROXY_BUFFER_HARD_CEILING`. NEVER set from production code.\n */\nlet proxyBufferHardCeilingOverride: number | undefined;\n\n/** @internal test-only — see `proxyBufferHardCeilingOverride`. */\nexport function setProxyBufferHardCeilingForTests(value: number | undefined): void {\n proxyBufferHardCeilingOverride = value;\n}\n\n/** Effective hard ceiling, honoring any active test-only override. */\nfunction effectiveHardCeiling(): number {\n return proxyBufferHardCeilingOverride ?? PROXY_BUFFER_HARD_CEILING;\n}\n\nconst STRIP_HEADERS = new Set([\n // Hop-by-hop (RFC 2616 §13.5.1)\n \"connection\",\n \"keep-alive\",\n \"transfer-encoding\",\n \"te\",\n \"trailer\",\n \"upgrade\",\n \"proxy-authorization\",\n \"proxy-authenticate\",\n // Set by HTTP client from the target URL / body\n \"host\",\n \"content-length\",\n // Not relevant for LLM APIs; avoid leaking or mismatched encoding\n \"cookie\",\n \"accept-encoding\",\n // Mock-internal control headers — meaningless (and potentially confusing\n // or leaky) on a real provider's wire. x-aimock-chaos-* is stripped by\n // prefix in buildForwardHeaders below.\n \"x-test-id\",\n \"x-aimock-strict\",\n \"x-aimock-context\",\n]);\n\n/**\n * Headers stripped on egress ONLY when aimock minted the value. A\n * caller-supplied `x-request-id` is the caller's own trace key and forwards\n * verbatim; the id aimock invents for its access log and journal is\n * meaningless on a real provider's wire and must not be transmitted.\n */\nconst MINTED_ONLY_STRIP_HEADERS = new Set([\"x-request-id\"]);\n\n/**\n * Build the header set forwarded to an upstream provider from an incoming\n * request: everything except hop-by-hop, client-set, and mock-internal\n * headers (STRIP_HEADERS, plus the x-aimock-chaos-* prefix family, plus\n * MINTED_ONLY_STRIP_HEADERS when aimock minted the value). Shared\n * by the generic recorder proxy and the OpenRouter-video live lifecycle\n * proxy.\n */\nexport function buildForwardHeaders(req: http.IncomingMessage): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [name, val] of Object.entries(req.headers)) {\n const lower = name.toLowerCase();\n if (\n val === undefined ||\n STRIP_HEADERS.has(lower) ||\n (MINTED_ONLY_STRIP_HEADERS.has(lower) && hasMintedRequestId(req)) ||\n lower.startsWith(\"x-aimock-chaos-\") ||\n (isAuthenticatedRequest(req) && isRecognizedApiKeyHeader(lower))\n ) {\n continue;\n }\n out[name] = Array.isArray(val) ? val.join(\", \") : val;\n }\n return out;\n}\n\n/** Remove every casing of a header from an egress map. */\nexport function removeForwardHeader(headers: Record<string, string>, name: string): void {\n const lowerName = name.toLowerCase();\n for (const key of Object.keys(headers)) {\n if (key.toLowerCase() === lowerName) delete headers[key];\n }\n}\n\n/**\n * Set a header on an egress map ONLY if the inbound request did not already\n * supply it (in any casing). Used for content-negotiation defaults the mock\n * wants when the caller was not explicit. Never overwrites: a caller-sent\n * header may be covered by a request signature (AWS SigV4 and friends sign a\n * named subset of headers), so replacing one silently invalidates the request\n * upstream.\n */\nexport function setForwardHeaderDefault(\n headers: Record<string, string>,\n name: string,\n value: string,\n): void {\n const lowerName = name.toLowerCase();\n for (const key of Object.keys(headers)) {\n if (key.toLowerCase() === lowerName) return;\n }\n headers[name] = value;\n}\n\n/**\n * Construct the only safe egress map when inbound access control is enabled.\n * Test credentials are always removed; a static configured provider credential\n * is mandatory so an accepted test credential can never become an upstream one.\n */\nexport function prepareEgressHeaders(\n req: http.IncomingMessage,\n target: URL,\n providerKey: RecordProviderKey,\n configuredKey: string | undefined,\n): Record<string, string> | undefined {\n const headers = buildForwardHeaders(req);\n if (!isAuthenticatedRequest(req)) {\n applyProviderAuth(headers, target, providerKey, configuredKey);\n return headers;\n }\n for (const name of Object.keys(headers)) {\n if (isRecognizedApiKeyHeader(name)) delete headers[name];\n }\n return configuredKey && applyConfiguredProviderAuth(headers, target, providerKey, configuredKey)\n ? headers\n : undefined;\n}\n\n/**\n * Captured upstream response, exposed to the `beforeWriteResponse` hook so\n * callers can decide whether to relay it or mutate it (e.g. chaos injection).\n */\nexport interface ProxyCapturedResponse {\n status: number;\n contentType: string;\n body: Buffer;\n}\n\nexport interface ProxyOptions {\n /**\n * Called after the upstream response has been captured and recorded, but\n * before the relay to the client. Contract when the hook returns `true`:\n * 1. It wrote its own response body on `res`.\n * 2. It journaled the outcome (proxyAndRecord will NOT journal it).\n * 3. proxyAndRecord skips its default relay and returns `\"handled_by_hook\"`.\n *\n * Returning `false` (or omitting the hook) lets proxyAndRecord relay the\n * upstream response normally and leaves journaling to the caller via the\n * `\"relayed\"` outcome. Rejected promises propagate and leave the response\n * unwritten.\n *\n * NOT invoked when the upstream response was streamed progressively to the\n * client (SSE, NDJSON, or binary event streams) — the bytes are already on\n * the wire and can't be mutated.\n * Callers that need to observe the bypass should pass `onHookBypassed`.\n */\n beforeWriteResponse?: (response: ProxyCapturedResponse) => boolean | Promise<boolean>;\n /**\n * Called when `beforeWriteResponse` was provided but could not be invoked\n * because the upstream response was streamed to the client progressively.\n * The hook was rolled + wired but the bytes left before it could fire.\n * Intended for observability (log/metric/journal annotation) — proxyAndRecord\n * still returns `\"relayed\"`.\n */\n onHookBypassed?: (reason: \"sse_streamed\" | \"ndjson_streamed\" | \"binary_streamed\") => void;\n}\n\n/**\n * Outcome of a proxyAndRecord call, returned so the caller can decide whether\n * to journal, fall through, or stop — without sharing a mutable flag with the\n * `beforeWriteResponse` hook.\n *\n * - `\"not_configured\"` — no upstream URL for this provider; caller should fall\n * through to its next branch (typically strict/404).\n * - `\"relayed\"` — the default code path wrote a response (upstream success or\n * synthesized 502 error). Caller should journal the outcome.\n * - `\"handled_by_hook\"` — the hook wrote + journaled its own response. Caller\n * should not double-journal.\n */\nexport type ProxyOutcome = \"not_configured\" | \"relayed\" | \"handled_by_hook\";\n\n/**\n * Result of `persistFixture`:\n * - `\"skipped\"` — proxy-only mode; the caller has nothing else to do.\n * - `\"written\"` — fixture saved to `filepath` and (unless the match was empty)\n * registered into the in-memory cache so the next identical request matches.\n * - `\"failed\"` — filesystem write failed. Caller decides how to surface it\n * (e.g. setting `X-AIMock-Record-Error` on a relay response).\n */\nexport type PersistFixtureResult =\n | { kind: \"skipped\" }\n | { kind: \"written\"; filepath: string }\n | { kind: \"failed\"; error: string };\n\n/**\n * Make an arbitrary string safe to set as an HTTP header value. Node's\n * `res.setHeader` throws ERR_INVALID_CHAR on anything outside Latin-1 (plus\n * control characters) — and persist errors embed filesystem paths verbatim,\n * so a Unicode fixture path would turn a recoverable record failure into a\n * 500. Out-of-range characters are replaced with `?` rather than stripped so\n * the value's shape stays legible.\n */\nexport function sanitizeHeaderValue(value: string): string {\n return value.replace(/[^\\t\\x20-\\x7e\\x80-\\xff]/g, \"?\");\n}\n\n/**\n * Resolve a `content-type` header value to a single string. Node's\n * `http.IncomingHttpHeaders` types `content-type` as `string | string[]`, and\n * a constructed/proxied header object CAN carry an array (e.g. duplicated\n * header lines surfaced by some HTTP stacks). `join(\", \")`-ing such an array\n * produces a malformed `Content-Type: application/json, text/html` value — pick\n * the FIRST element instead, which is the value the client should act on.\n * Returns `\"\"` when the header is absent or an empty array.\n */\nexport function pickContentType(contentType: string | string[] | undefined): string {\n if (Array.isArray(contentType)) {\n return contentType.length > 0 ? contentType[0] : \"\";\n }\n return contentType ?? \"\";\n}\n\n/**\n * Write a built fixture to disk (snapshot vs. timestamp file layout) and, when\n * the match is non-empty, register it in the in-memory cache so subsequent\n * identical requests match. Extracted from `proxyAndRecord` so the fal\n * queue-walk recorder (which makes multiple upstream calls before knowing the\n * final body) can share the same persistence behavior without re-implementing\n * snapshot-mode merging and warnings.\n */\nexport function persistFixture(opts: {\n record: RecordConfig;\n providerKey: RecordProviderKey;\n testId: string;\n /** Separate Live captures from HTTP OpenAI snapshots. */\n filePrefix?: \"openai-live\";\n fixture: Fixture;\n fixtures: Fixture[];\n warnings?: string[];\n logger: Logger;\n}): PersistFixtureResult {\n const { record, providerKey, testId, fixture, fixtures, warnings = [], logger } = opts;\n const fileKey = opts.filePrefix ?? providerKey;\n\n // Match criteria with no userMessage / inputText / endpoint will not match\n // any future request — warn, then save to disk for inspection but skip the\n // in-memory registration so a defective fixture doesn't shadow real ones.\n // turnIndex/hasToolResult are pure multi-turn disambiguators on their own.\n const m = fixture.match;\n const isEmptyMatch =\n m.userMessage === undefined && m.inputText === undefined && m.endpoint === undefined;\n\n if (record.proxyOnly) {\n logger.info(`Proxied ${providerKey} request (proxy-only mode)`);\n return { kind: \"skipped\" };\n }\n\n // Warned only past the proxy-only early-return: under proxy-only nothing is\n // persisted or registered, so an empty match has no consequence there.\n if (isEmptyMatch) {\n logger.warn(\"Recorded fixture has empty match criteria — skipping in-memory registration\");\n }\n\n const fixturePath = record.fixturePath ?? \"./fixtures/recorded\";\n let isSnapshotMode = testId !== DEFAULT_TEST_ID;\n let filepath: string;\n let mergeExisting = false;\n\n if (isSnapshotMode) {\n const slug = slugifyTestId(testId);\n if (!slug) {\n // Slug resolved to empty (e.g. testId was all punctuation) — fall back\n // to timestamp-based recording so we still capture the fixture.\n isSnapshotMode = false;\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n filepath = path.join(\n fixturePath,\n `${fileKey}-${timestamp}-${crypto.randomUUID().slice(0, 8)}.json`,\n );\n } else {\n filepath = path.join(fixturePath, slug, `${fileKey}.json`);\n mergeExisting = true;\n }\n } else {\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const timestampFile = `${fileKey}-${timestamp}-${crypto.randomUUID().slice(0, 8)}.json`;\n // The context becomes a directory segment, but it originates from the\n // attacker-controllable `X-AIMock-Context` header — slugify it (mirroring\n // testId above) so `../`, separators, and absolute prefixes can't escape\n // the fixtures base dir. An empty slug → no context segment.\n const contextSegment = fixture.match.context\n ? slugifyContext(fixture.match.context)\n : undefined;\n filepath = contextSegment\n ? path.join(fixturePath, contextSegment, timestampFile)\n : path.join(fixturePath, timestampFile);\n }\n\n const fileWarnings = [\n ...(isEmptyMatch ? [\"Empty match criteria — this fixture will not match any request\"] : []),\n ...warnings,\n ];\n\n let tmpPath: string | undefined;\n try {\n fs.mkdirSync(path.dirname(filepath), { recursive: true });\n\n // Auth headers are forwarded to upstream but excluded from saved fixtures.\n // The persisted fixture is always the real upstream response, even when\n // chaos later mutates the relay; replay must see what upstream said.\n // Warnings are persisted as a `_warnings` JSON array (authoritative,\n // non-fragmenting) AND a legacy \"; \"-joined `_warning` string (kept for\n // backward-compatible readers). The array is the source of truth on a\n // snapshot merge: a single warning that itself contains \"; \" (e.g.\n // \"captured A; then B\") would be fragmented into two bogus entries if the\n // joined string were split back apart on merge. Carrying the array forward\n // avoids that round-trip fragmentation entirely.\n let fileContent: {\n [key: string]: unknown;\n fixtures: unknown[];\n _warning?: string;\n _warnings?: string[];\n };\n if (mergeExisting && fs.existsSync(filepath)) {\n try {\n const existing = JSON.parse(fs.readFileSync(filepath, \"utf-8\"));\n // Guard the spread: a non-array `fixtures` (e.g. a string from a\n // hand-edited file) would silently spread into single characters and\n // mangle the merged file. Treat it like the corrupt-JSON case below.\n const existingFixtures = Array.isArray(existing.fixtures)\n ? (existing.fixtures as unknown[])\n : undefined;\n if (existingFixtures === undefined && existing.fixtures !== undefined) {\n logger.warn(\n `Existing fixture file ${filepath} has a non-array \"fixtures\" — discarding it and starting fresh`,\n );\n }\n // Keep every top-level key the recorder does not own (e.g. a\n // hand-added `mcpFakes` block). The recorder owns `fixtures`,\n // `_warnings` and `_warning`; those are rebuilt below, so a stale\n // copy is never carried over verbatim.\n const keptKeys: Record<string, unknown> = {};\n if (existing !== null && typeof existing === \"object\" && !Array.isArray(existing)) {\n for (const [key, value] of Object.entries(existing as Record<string, unknown>)) {\n if (key === \"fixtures\" || key === \"_warnings\" || key === \"_warning\") continue;\n keptKeys[key] = value;\n }\n }\n fileContent = { ...keptKeys, fixtures: [...(existingFixtures ?? []), fixture] };\n // Carry existing warnings forward — a later clean capture merging into\n // the same snapshot file must not erase an earlier capture's warning\n // (e.g. an over-cap b64 omission). Prefer the new `_warnings` array;\n // fall back to the legacy \"; \"-joined `_warning` string for files\n // written before this format change. The legacy split can still\n // fragment an embedded-\"; \" warning in a pre-existing file, but new\n // captures no longer create that hazard.\n if (Array.isArray(existing._warnings)) {\n fileWarnings.unshift(\n ...(existing._warnings as unknown[]).filter((w): w is string => typeof w === \"string\"),\n );\n } else if (typeof existing._warning === \"string\" && existing._warning) {\n fileWarnings.unshift(...existing._warning.split(\"; \"));\n }\n } catch (mergeErr) {\n const msg = mergeErr instanceof Error ? mergeErr.message : \"unknown\";\n logger.warn(`Could not read existing fixture file ${filepath} (${msg}) — overwriting`);\n fileContent = { fixtures: [fixture] };\n }\n } else {\n fileContent = { fixtures: [fixture] };\n }\n if (fileWarnings.length > 0) {\n // Exact-duplicate warnings (e.g. repeated over-cap captures merging into\n // the same snapshot file) collapse to one entry. Emit BOTH the\n // authoritative array and the legacy joined string.\n const dedupedWarnings = [...new Set(fileWarnings)];\n fileContent._warnings = dedupedWarnings;\n fileContent._warning = dedupedWarnings.join(\"; \");\n }\n // Atomic write: write to temp file then rename to avoid read-modify-write\n // races. Keep synchronous — for streamed responses the HTTP reply is\n // already on the wire, so async writes would race with callers checking\n // the filesystem before the fixture has landed.\n tmpPath = filepath + \".tmp.\" + crypto.randomUUID();\n fs.writeFileSync(tmpPath, JSON.stringify(fileContent, null, 2), {\n encoding: \"utf-8\",\n flag: \"wx\",\n mode: 0o600,\n });\n fs.renameSync(tmpPath, filepath);\n tmpPath = undefined;\n\n if (!isEmptyMatch) {\n fixtures.push(fixture);\n }\n logger.warn(`Response recorded → ${filepath}`);\n return { kind: \"written\", filepath };\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown filesystem error\";\n logger.error(`Failed to save fixture to disk: ${msg}`);\n return { kind: \"failed\", error: msg };\n } finally {\n if (tmpPath) {\n try {\n fs.unlinkSync(tmpPath);\n } catch {\n /* Preserve the original persistence error. */\n }\n }\n }\n}\n\n/**\n * Proxy an unmatched request to the real upstream provider, record the\n * response as a fixture on disk and in memory, then relay the response\n * back to the original client.\n */\nexport async function proxyAndRecord(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n request: ChatCompletionRequest,\n providerKey: RecordProviderKey,\n pathname: string,\n fixtures: Fixture[],\n defaults: {\n record?: RecordConfig;\n logger: Logger;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n responsesTools?: ResponsesToolsMode;\n },\n rawBody?: string,\n options?: ProxyOptions,\n): Promise<ProxyOutcome> {\n const record = defaults.record;\n if (!record) return \"not_configured\";\n\n const providers = record.providers;\n // Gemini Interactions uses the same upstream API as Gemini (identical base URL\n // and auth), so we remap the provider key to reuse the configured Gemini URL.\n const lookupKey = providerKey === \"gemini-interactions\" ? \"gemini\" : providerKey;\n const upstreamUrl = providers[lookupKey];\n\n if (!upstreamUrl) {\n defaults.logger.warn(`No upstream URL configured for provider \"${providerKey}\" — cannot proxy`);\n return \"not_configured\";\n }\n\n let target: URL;\n try {\n target = resolveUpstreamUrl(upstreamUrl, pathname);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n defaults.logger.error(\n `Invalid upstream URL for provider \"${providerKey}\": ${upstreamUrl} (${msg})`,\n );\n writeErrorResponse(\n res,\n 502,\n JSON.stringify({\n error: { message: `Invalid upstream URL: ${upstreamUrl}`, type: \"proxy_error\" },\n }),\n );\n return \"relayed\";\n }\n\n defaults.logger.warn(`NO FIXTURE MATCH — proxying to ${upstreamUrl}${pathname}`);\n\n // Forward all request headers except hop-by-hop and client-set ones.\n const forwardHeaders = prepareEgressHeaders(\n req,\n target,\n providerKey,\n record.providerKeys?.[lookupKey],\n );\n if (!forwardHeaders) {\n writeErrorResponse(\n res,\n 502,\n JSON.stringify({\n error: { message: \"No configured provider credential\", type: \"proxy_error\" },\n }),\n );\n return \"relayed\";\n }\n\n const requestBody = rawBody ?? JSON.stringify(request);\n\n // Make upstream request\n let upstreamStatus: number;\n let upstreamHeaders: http.IncomingHttpHeaders;\n let upstreamBody: string;\n let rawBuffer: Buffer;\n let bufferTruncated = false;\n let truncationCap: \"byte\" | \"frame\" | undefined;\n let totalBytes = 0;\n let hardCeilingExceeded = false;\n\n // Track whether we streamed SSE progressively to the client; if so,\n // skip the final res.writeHead/res.end relay at the bottom of this fn.\n let streamedToClient = false;\n let clientDisconnected = false;\n let sawDone = false;\n let frameTimestamps: number[] = [];\n let streamStartTime = 0;\n const maxProxyBufferBytes = clampMaxBufferBytes(record.maxProxyBufferBytes);\n const maxProxyBufferFrames = clampMaxBufferFrames(record.maxProxyBufferFrames);\n try {\n const result = await makeUpstreamRequest(\n target,\n forwardHeaders,\n requestBody,\n res,\n req.method,\n defaults.logger,\n { upstreamTimeoutMs: record.upstreamTimeoutMs, bodyTimeoutMs: record.bodyTimeoutMs },\n maxProxyBufferBytes,\n maxProxyBufferFrames,\n );\n upstreamStatus = result.status;\n upstreamHeaders = result.headers;\n upstreamBody = result.body;\n rawBuffer = result.rawBuffer;\n streamedToClient = result.streamedToClient;\n clientDisconnected = result.clientDisconnected;\n sawDone = result.sawDone;\n frameTimestamps = result.frameTimestamps;\n streamStartTime = result.streamStartTime;\n bufferTruncated = result.bufferTruncated;\n truncationCap = result.truncationCap;\n totalBytes = result.totalBytes;\n hardCeilingExceeded = result.hardCeilingExceeded;\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown proxy error\";\n defaults.logger.error(`Proxy request failed: ${msg}`);\n if (!res.headersSent) {\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: { message: `Proxy to upstream failed: ${msg}`, type: \"proxy_error\" },\n }),\n );\n } else {\n // Streaming headers (200) are already on the wire, so we cannot change the\n // status — but we MUST NOT call res.end(), which the client reads as a\n // clean EOF and treats as a complete (silently truncated) response.\n // Destroy the connection instead so the client sees an aborted stream and\n // can surface the upstream failure rather than acting on partial data.\n res.destroy();\n }\n return \"relayed\";\n }\n\n // Buffer cap tripped: skip collapse + recording (the in-memory buffer can't\n // be safely/faithfully journaled), but ALWAYS deliver a faithful response to\n // the client. The cap means \"don't journal\", not \"don't answer\".\n // - PROGRESSIVE stream: the bytes were already teed to the client live, so\n // nothing more to write — just end if still open.\n // - NON-progressive response, UNDER the hard ceiling: makeUpstreamRequest\n // kept the FULL body (bounded by PROXY_BUFFER_HARD_CEILING) precisely so we\n // can relay it here. Relay the real body so a large single-shot JSON\n // (embeddings / fal / image-b64) reaches the client intact — but NORMALIZE\n // the status (success→200 / error→502) exactly like every other relay path\n // so upstream provider details don't leak through this branch alone.\n // - NON-progressive response, OVER the hard ceiling: the retained buffer is\n // PARTIAL (we stopped buffering at the ceiling and there is no live tee),\n // so relaying it would present a truncated body as success. FAIL LOUD with\n // a 502 instead — never deliver a silently-truncated body as 2xx.\n if (bufferTruncated) {\n const capDetail =\n truncationCap === \"frame\"\n ? `the ${maxProxyBufferFrames}-frame cap`\n : `the ${maxProxyBufferBytes}-byte cap`;\n if (!streamedToClient && hardCeilingExceeded && !res.headersSent) {\n const ceilingMiB = Math.round(PROXY_BUFFER_HARD_CEILING / (1024 * 1024));\n defaults.logger.error(\n `Upstream response exceeded the ${ceilingMiB} MiB proxy hard ceiling (saw ${totalBytes} bytes) on a non-progressive body — cannot relay the full body and refusing to relay a truncated one; returning 502`,\n );\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: `Upstream response exceeds ${ceilingMiB}MiB proxy ceiling`,\n type: \"proxy_error\",\n },\n }),\n );\n return \"relayed\";\n }\n defaults.logger.warn(\n `Upstream response exceeded ${capDetail} (saw ${totalBytes} bytes) — relayed to client, recording skipped`,\n );\n if (!streamedToClient && !res.headersSent) {\n // Normalize the relayed status like the under-cap relay paths\n // (success→200, error→502) so this branch does not leak a raw upstream\n // 429/503/etc. The full real body (under the hard ceiling) is relayed.\n const clientStatus = upstreamStatus >= 200 && upstreamStatus < 300 ? 200 : 502;\n const relayHeaders: Record<string, string> = {};\n const ct = upstreamHeaders[\"content-type\"];\n const ctStr = Array.isArray(ct) ? ct.join(\", \") : (ct ?? \"\");\n relayHeaders[\"Content-Type\"] = ctStr || \"application/json\";\n res.writeHead(clientStatus, relayHeaders);\n res.end(rawBuffer);\n } else if (!res.writableEnded) {\n res.end();\n }\n return \"relayed\";\n }\n\n // Detect streaming response and collapse if necessary.\n // NOTE: collapse buffers the upstream body in memory up to maxProxyBufferBytes\n // (see makeUpstreamRequest). Over-cap responses short-circuit above, so the\n // buffer reaching here is bounded and safe to stringify/collapse.\n const contentType = upstreamHeaders[\"content-type\"];\n const ctString = pickContentType(contentType);\n const isBinaryStream = ctString.toLowerCase().includes(\"application/vnd.amazon.eventstream\");\n // OpenAI Responses namespaces and custom tool calls are recorded only with\n // responsesTools \"extended\"; otherwise the recording is what earlier\n // releases wrote (namespaces dropped, custom calls not recorded).\n const collapsedRaw = collapseStreamingResponseWithResponsesTools(\n ctString,\n providerKey,\n isBinaryStream ? rawBuffer : upstreamBody,\n defaults.logger,\n );\n const collapsed =\n collapsedRaw && defaults.responsesTools !== \"extended\"\n ? withoutResponsesToolFields(collapsedRaw)\n : collapsedRaw;\n\n let fixtureResponse: FixtureResponse;\n\n // TTS response — binary audio, not JSON. Classify on the audio content-type\n // when upstream succeeded (2xx), regardless of body length: a zero-length\n // audio body is still an audio response (empty audio), NOT an opaque\n // proxy_error — letting it fall through to the JSON path would mis-record it.\n // A non-2xx audio response (error JSON in the body) is left to the JSON/error\n // path below.\n const isAudioResponse = ctString.toLowerCase().startsWith(\"audio/\");\n const isAudioSuccess = upstreamStatus >= 200 && upstreamStatus < 300;\n if (isAudioResponse && isAudioSuccess) {\n // Derive format from Content-Type (audio/mpeg→mp3, audio/opus→opus, etc.)\n const audioFormat = ctString\n .toLowerCase()\n .replace(\"audio/\", \"\")\n .replace(\"mpeg\", \"mp3\")\n .split(\";\")[0]\n .trim();\n if (rawBuffer.length === 0) {\n defaults.logger.warn(\n \"Audio response had a zero-length body — recording an empty audio fixture\",\n );\n }\n fixtureResponse = {\n audio: rawBuffer.toString(\"base64\"),\n ...(audioFormat && audioFormat !== \"mp3\" ? { format: audioFormat } : {}),\n };\n } else if (collapsed) {\n // Streaming response — use collapsed result\n defaults.logger.warn(`Streaming response detected (${ctString}) — collapsing to fixture`);\n if (collapsed.truncated) {\n defaults.logger.warn(\"Bedrock EventStream: CRC mismatch — response may be truncated\");\n }\n if (collapsed.droppedChunks && collapsed.droppedChunks > 0) {\n defaults.logger.warn(\n `${collapsed.droppedChunks} chunk(s) dropped during stream collapse${collapsed.firstDroppedSample ? ` — first: ${collapsed.firstDroppedSample}` : \"\"}`,\n );\n }\n if (collapsed.harmonyUnparsed) {\n defaults.logger.warn(\n `Harmony tokens present but unparseable — content preserved verbatim${collapsed.harmonyNote ? ` (${collapsed.harmonyNote})` : \"\"}`,\n );\n }\n // Provider-reported usage captured from the stream's final usage frame\n // (#368). Persisted on the text / toolCall fixture shapes so replay serves\n // the REAL token counts instead of the `ceil(len/4)` estimate, and so\n // OpenRouter's `usage.cost` round-trips to the replayed usage chunk /\n // completion envelope. Omitted when the stream carried no usage, keeping\n // every pre-#368 recorded fixture byte-identical. Transcription fixtures\n // carry their own `usage` slot, so the transcription branch below is left\n // untouched. The audio shape (`AudioResponse` in types.ts) has NO usage\n // field, so the audio branch cannot carry `usage` at all — inert today (a\n // collapse never yields both `audioB64` and a stream usage frame), but a\n // future collapser that set both would drop the usage on the audio path\n // with no compiler signal.\n const collapsedUsage = sanitizeRecordedUsage(collapsed.usage);\n const usageSpread = collapsedUsage ? { usage: collapsedUsage } : {};\n // Audio from streamed inlineData (e.g. Gemini SSE with audio parts).\n // A single Gemini turn can interleave audio with a functionCall and/or\n // text/thought parts; preserve those companion modalities so the tool call\n // / content / reasoning are not silently dropped when audio is present.\n if (collapsed.transcription) {\n fixtureResponse = { transcription: collapsed.transcription };\n } else if (collapsed.audioB64) {\n const audioToolCallsSpread =\n collapsed.toolCalls && collapsed.toolCalls.length > 0\n ? {\n toolCalls: collapsed.toolCalls.map((tc) => ({\n ...tc,\n name: tc.name ?? \"\",\n arguments: tc.arguments ?? \"{}\",\n })),\n }\n : {};\n const audioContentSpread = collapsed.content ? { content: collapsed.content } : {};\n const audioReasoningSpread = collapsed.reasoning ? { reasoning: collapsed.reasoning } : {};\n fixtureResponse = {\n audio: {\n b64Json: collapsed.audioB64,\n contentType: collapsed.audioMimeType ?? \"audio/mpeg\",\n },\n ...audioToolCallsSpread,\n ...audioContentSpread,\n ...audioReasoningSpread,\n };\n } else if (\n collapsed.content === \"\" &&\n (!collapsed.toolCalls || collapsed.toolCalls.length === 0) &&\n !collapsed.customToolCalls?.length\n ) {\n defaults.logger.warn(\"Stream collapse produced empty content — fixture may be incomplete\");\n const reasoningSpread = collapsed.reasoning ? { reasoning: collapsed.reasoning } : {};\n // Carry the real Anthropic thinking-block signature only when reasoning is\n // also present (a bare signature has nothing to attach to on replay), so a\n // recorded thinking turn replays its actual signature instead of the\n // round-trip-safe placeholder.\n const reasoningSignatureSpread =\n collapsed.reasoning && collapsed.reasoningSignature\n ? { reasoningSignature: collapsed.reasoningSignature }\n : {};\n logDroppedReasoningSignature(\n defaults.logger,\n collapsed.reasoning,\n collapsed.reasoningSignature,\n );\n // Redacted-thinking blocks carry their OWN encrypted reasoning, so they are\n // carried independently of any plaintext `reasoning` (a turn can have only\n // redacted thinking) so the recorded turn round-trips its redacted blocks.\n const redactedThinkingSpread = collapsed.redactedThinking?.length\n ? { redactedThinking: collapsed.redactedThinking }\n : {};\n const webSearchesSpread = collapsed.webSearches?.length\n ? { webSearches: collapsed.webSearches }\n : {};\n fixtureResponse = {\n content: collapsed.content ?? \"\",\n ...reasoningSpread,\n ...reasoningSignatureSpread,\n ...redactedThinkingSpread,\n ...webSearchesSpread,\n ...usageSpread,\n };\n } else {\n const reasoningSpread = collapsed.reasoning ? { reasoning: collapsed.reasoning } : {};\n // Carry the real Anthropic thinking-block signature only when reasoning is\n // also present; see the empty-content branch above.\n const reasoningSignatureSpread =\n collapsed.reasoning && collapsed.reasoningSignature\n ? { reasoningSignature: collapsed.reasoningSignature }\n : {};\n logDroppedReasoningSignature(\n defaults.logger,\n collapsed.reasoning,\n collapsed.reasoningSignature,\n );\n // Redacted-thinking blocks carry their OWN encrypted reasoning, so they are\n // carried independently of any plaintext `reasoning`; see the empty-content\n // branch above.\n const redactedThinkingSpread = collapsed.redactedThinking?.length\n ? { redactedThinking: collapsed.redactedThinking }\n : {};\n const webSearchesSpread = collapsed.webSearches?.length\n ? { webSearches: collapsed.webSearches }\n : {};\n if (collapsed.customToolCalls?.length) {\n // responsesTools \"extended\" only: a turn with OpenAI Responses custom\n // tool calls. `toolCalls` (possibly empty) keeps the function calls,\n // so earlier releases replay the recording exactly as they replay\n // their own (they ignore the new keys). `responsesBlocks` replaces\n // `blocks` when the collapser needed it to keep the order.\n const toolCalls = (collapsed.toolCalls ?? []).map(sanitizeRecordedToolCall);\n const customToolCalls = collapsed.customToolCalls.map((tc) => ({\n ...tc,\n name: tc.name ?? \"\",\n input: tc.input ?? \"\",\n }));\n const orderSpread = collapsed.responsesBlocks?.length\n ? { responsesBlocks: collapsed.responsesBlocks }\n : collapsed.blocks?.length\n ? { blocks: collapsed.blocks }\n : {};\n fixtureResponse = {\n ...(collapsed.content ? { content: collapsed.content } : {}),\n toolCalls,\n customToolCalls,\n ...orderSpread,\n ...reasoningSpread,\n ...reasoningSignatureSpread,\n ...redactedThinkingSpread,\n ...webSearchesSpread,\n ...usageSpread,\n };\n } else if (collapsed.toolCalls && collapsed.toolCalls.length > 0) {\n const sanitizedToolCalls = collapsed.toolCalls.map(sanitizeRecordedToolCall);\n if (collapsed.content) {\n // Both content and toolCalls present — save as ContentWithToolCallsResponse.\n //\n // Ordered `blocks` (#274) is persisted ONLY when the collapser\n // classified the stream as interleaved — a tool-call delta appeared\n // strictly before the first content delta, OR a content delta\n // appeared after any tool-call delta. The collapser encodes exactly\n // that rule: it sets `collapsed.blocks` only in those cases and\n // leaves it undefined otherwise. So the recorder simply spreads it\n // when present; an ordinary text-then-tools (or text-only) stream has\n // no `blocks` and persists the legacy shape byte-identically.\n const blocksSpread = collapsed.blocks?.length ? { blocks: collapsed.blocks } : {};\n fixtureResponse = {\n content: collapsed.content,\n toolCalls: sanitizedToolCalls,\n ...blocksSpread,\n ...reasoningSpread,\n ...reasoningSignatureSpread,\n ...redactedThinkingSpread,\n ...webSearchesSpread,\n ...usageSpread,\n };\n } else {\n fixtureResponse = {\n toolCalls: sanitizedToolCalls,\n ...reasoningSpread,\n ...reasoningSignatureSpread,\n ...redactedThinkingSpread,\n ...webSearchesSpread,\n ...usageSpread,\n };\n }\n } else {\n fixtureResponse = {\n content: collapsed.content ?? \"\",\n ...reasoningSpread,\n ...reasoningSignatureSpread,\n ...redactedThinkingSpread,\n ...webSearchesSpread,\n ...usageSpread,\n };\n }\n }\n } else {\n // Non-streaming — try to parse as JSON\n let parsedResponse: unknown = null;\n try {\n parsedResponse = JSON.parse(upstreamBody);\n } catch (parseErr) {\n const msg = parseErr instanceof Error ? parseErr.message : \"unknown\";\n defaults.logger.warn(\n `Upstream response is not valid JSON (${msg}) — saving as error fixture`,\n );\n }\n // fal.ai and ElevenLabs Voice Design/create/get/delete return arbitrary JSON shapes\n // (previews with embedded base64 audio, voice objects). Round-trip the\n // payload verbatim instead of letting buildFixtureResponse mis-classify\n // them as ImageResponse / VideoResponse / TranscriptionResponse.\n if (\n (request._endpointType === \"fal\" ||\n request._endpointType === \"elevenlabs-voice-design\" ||\n request._endpointType === \"elevenlabs-voice\" ||\n request._endpointType === \"elevenlabs-voice-get\" ||\n request._endpointType === \"elevenlabs-voice-delete\") &&\n parsedResponse !== null\n ) {\n const obj = parsedResponse as Record<string, unknown>;\n const isErrorShape =\n typeof obj.error === \"object\" &&\n obj.error !== null &&\n typeof (obj.error as Record<string, unknown>).message === \"string\";\n if (isErrorShape) {\n const err = obj.error as Record<string, unknown>;\n fixtureResponse = {\n error: {\n message: String(err.message ?? \"Unknown error\"),\n type: String(err.type ?? \"api_error\"),\n code: err.code ? String(err.code) : undefined,\n },\n status: upstreamStatus,\n };\n } else {\n fixtureResponse = { json: parsedResponse, status: upstreamStatus };\n }\n } else {\n // NOTE: base64 embeddings are decoded unconditionally inside\n // buildFixtureResponse regardless of the request's `encoding_format`, so\n // there is no need to re-parse it here — it was a dead param.\n fixtureResponse = buildFixtureResponse(\n parsedResponse,\n upstreamStatus,\n defaults.logger,\n request,\n );\n }\n }\n\n // Client may have closed its socket before upstream fired `end`.\n // Distinguish two cases based on whether an SSE terminal frame was seen:\n // - sawDone=true: client closed after consuming `data: [DONE]` or a typed\n // `transcript.text.done` event.\n // Upstream ran to completion; the buffered body is intact. Log and\n // proceed to persist the full fixture.\n // - sawDone=false: genuine mid-stream abort. The buffered body is partial;\n // saving it would produce a corrupt fixture. Skip fixture persistence.\n if (clientDisconnected) {\n if (!sawDone) {\n defaults.logger.warn(\n \"Client disconnected mid-stream — skipping fixture save to avoid truncated data\",\n );\n return \"relayed\";\n }\n defaults.logger.warn(\n \"Client closed connection before upstream end — upstream response completed, recording full fixture\",\n );\n }\n\n // Build RecordedTimings from frame timestamps captured during streaming.\n // Requires at least 2 timestamps (first frame + at least one more) to\n // produce meaningful timing data.\n let recordedTimings: RecordedTimings | undefined;\n if (frameTimestamps.length > 1) {\n const ts = frameTimestamps;\n recordedTimings = {\n ttftMs: ts[0] - streamStartTime,\n interChunkDelaysMs: ts.slice(1).map((t, i) => t - ts[i]),\n totalDurationMs: ts[ts.length - 1] - streamStartTime,\n };\n }\n\n const matchRequest = defaults.requestTransform ? defaults.requestTransform(request) : request;\n const metadata = buildFixtureMetadata(request);\n const fixture: Fixture = {\n match: buildFixtureMatch(matchRequest, defaults.record),\n response: fixtureResponse,\n ...(recordedTimings && { recordedTimings }),\n ...(metadata && { metadata }),\n };\n\n const persistWarnings: string[] = [];\n if (collapsed?.truncated) {\n persistWarnings.push(\"Stream response was truncated — fixture may be incomplete\");\n }\n const persistResult = persistFixture({\n record,\n providerKey,\n testId: getTestId(req),\n fixture,\n fixtures,\n warnings: persistWarnings,\n logger: defaults.logger,\n });\n if (persistResult.kind === \"failed\") {\n if (!res.headersSent) {\n res.setHeader(\"X-AIMock-Record-Error\", sanitizeHeaderValue(persistResult.error));\n } else {\n defaults.logger.warn(`Cannot set X-AIMock-Record-Error header — headers already sent`);\n }\n defaults.logger.warn(`Response relayed but NOT saved to disk — see error above`);\n }\n\n // Relay upstream response to client (skip when the response was already\n // streamed progressively by makeUpstreamRequest — headers and body are\n // already on the wire).\n if (streamedToClient) {\n // The hook can't run because the body is already on the wire. Surface\n // the bypass so the caller (typically the chaos layer) can record it —\n // otherwise a configured chaos action silently no-ops on streamed traffic.\n if (options?.beforeWriteResponse && options.onHookBypassed) {\n const bypassReason: \"sse_streamed\" | \"ndjson_streamed\" | \"binary_streamed\" = isBinaryStream\n ? \"binary_streamed\"\n : ctString.toLowerCase().includes(\"application/x-ndjson\")\n ? \"ndjson_streamed\"\n : \"sse_streamed\";\n try {\n options.onHookBypassed(bypassReason);\n } catch (err) {\n defaults.logger.warn(\n `onHookBypassed callback threw: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n } else {\n // Give the caller a chance to mutate or replace the response before relay.\n // Used by the chaos layer to turn a successful proxy into a malformed body.\n // `body` is the raw upstream bytes so binary payloads survive round-tripping.\n if (options?.beforeWriteResponse) {\n let handled: boolean | undefined;\n try {\n handled = await options.beforeWriteResponse({\n status: upstreamStatus,\n contentType: ctString,\n body: rawBuffer,\n });\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(`beforeWriteResponse hook failed for ${providerKey}: ${msg}`);\n }\n if (handled) return \"handled_by_hook\";\n }\n\n // Normalize status codes for the client: aimock acts as a gateway, so\n // upstream provider details (429 rate-limits, 503 outages, etc.) should\n // not leak. Successes → 200, errors → 502 (Bad Gateway).\n const clientStatus = upstreamStatus >= 200 && upstreamStatus < 300 ? 200 : 502;\n const isAudioRelay = ctString.toLowerCase().startsWith(\"audio/\");\n // When an upstream error (non-2xx) is relayed for an audio endpoint, the\n // body is typically a JSON error object — override the content-type so\n // clients don't try to decode JSON as audio.\n const relayHeaders: Record<string, string> = {};\n const clientCt =\n (clientStatus >= 200 && clientStatus < 300) || !isAudioRelay\n ? ctString || \"application/json\"\n : \"application/json\";\n if (clientCt) {\n relayHeaders[\"Content-Type\"] = clientCt;\n }\n res.writeHead(clientStatus, relayHeaders);\n res.end(isBinaryStream || isAudioRelay ? rawBuffer : upstreamBody);\n }\n\n return \"relayed\";\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Decodes a sequence of byte chunks to UTF-8 text for SSE/NDJSON frame\n * splitting on the streamed-capture path. Wraps Node's StringDecoder so a\n * multibyte UTF-8 character (CJK, emoji, ...) whose bytes are split across a\n * TCP chunk boundary buffers across chunks instead of decoding to U+FFFD\n * replacement characters — decoding each chunk independently with\n * Buffer#toString() would corrupt the recorded frame text.\n */\nexport class StreamingFrameDecoder {\n private decoder = new StringDecoder(\"utf8\");\n /** Decode a chunk, holding back any trailing partial multibyte sequence. */\n write(chunk: Buffer): string {\n return this.decoder.write(chunk);\n }\n /** Flush any buffered bytes once the stream has ended. */\n end(): string {\n return this.decoder.end();\n }\n}\n\n/**\n * Sanitize a configured timeout: non-finite or non-positive values fall back\n * to the default. Shared with the OpenRouter-video live lifecycle proxy so\n * `record.upstreamTimeoutMs` is clamped identically on every proxy path.\n */\nexport function clampTimeout(value: number | undefined, fallback: number): number {\n if (value == null || !Number.isFinite(value) || value <= 0) return fallback;\n return value;\n}\n\n/**\n * Sanitize the configured proxy-buffer cap: non-finite or non-positive values\n * fall back to the default. Also hard-clamps to just under V8's max string\n * length so a misconfigured large value can never permit an over-string-limit\n * buffer to reach `.toString()`.\n */\nexport function clampMaxBufferBytes(value: number | undefined): number {\n if (value == null || !Number.isFinite(value) || value <= 0) {\n return DEFAULT_MAX_PROXY_BUFFER_BYTES;\n }\n return Math.min(value, PROXY_BUFFER_HARD_CEILING);\n}\n\n/**\n * Sanitize the configured proxy-buffer frame cap: non-finite or non-positive\n * values fall back to the default. Bounds the count-indexed per-frame state\n * (`frameTimestamps` + parse buffers) that the byte cap cannot bound on its own.\n */\nexport function clampMaxBufferFrames(value: number | undefined): number {\n // INTENTIONAL DIVERGENCE from journal-max's `0 = unbounded` convention: here\n // 0 (and any non-positive / non-finite value) maps to the DEFAULT cap, never\n // \"unbounded\". The frame cap exists as leak-safety for never-ending proxy\n // streams; allowing 0 to disable it would reintroduce the exact unbounded\n // per-frame-state growth this cap guards against. Do NOT make 0 mean\n // unbounded.\n if (value == null || !Number.isFinite(value) || value <= 0) {\n return DEFAULT_MAX_PROXY_BUFFER_FRAMES;\n }\n return Math.floor(value);\n}\n\nfunction makeUpstreamRequest(\n target: URL,\n headers: Record<string, string>,\n body: string,\n clientRes?: http.ServerResponse,\n method: string = \"POST\",\n logger?: Logger,\n timeouts?: Pick<RecordConfig, \"upstreamTimeoutMs\" | \"bodyTimeoutMs\">,\n maxBufferBytes: number = DEFAULT_MAX_PROXY_BUFFER_BYTES,\n maxBufferFrames: number = DEFAULT_MAX_PROXY_BUFFER_FRAMES,\n): Promise<{\n status: number;\n headers: http.IncomingHttpHeaders;\n body: string;\n rawBuffer: Buffer;\n streamedToClient: boolean;\n clientDisconnected: boolean;\n frameTimestamps: number[];\n streamStartTime: number;\n /**\n * True when the upstream response exceeded `maxBufferBytes`. The client still\n * received every byte (the relay is independent of this buffer), but the\n * in-memory buffer was capped, so `body`/`rawBuffer` are partial and the\n * caller MUST skip collapse/recording.\n */\n bufferTruncated: boolean;\n /**\n * Which cap tripped truncation (`\"byte\"` or `\"frame\"`), or `undefined` when\n * not truncated. Lets the caller log accurately which budget was exceeded\n * rather than conflating the two.\n */\n truncationCap?: \"byte\" | \"frame\";\n /** Total bytes seen from upstream (may exceed the buffered amount when capped). */\n totalBytes: number;\n /**\n * True when a NON-progressive (non-teed) upstream body exceeded the proxy\n * hard ceiling, so the retained `rawBuffer` is PARTIAL. The caller MUST fail\n * loud (502) rather than relay this truncated body as a success status.\n */\n hardCeilingExceeded: boolean;\n /**\n * True when a terminal SSE frame was observed in the upstream stream before\n * the client disconnected. The terminal may be legacy `data: [DONE]` or the\n * typed OpenAI transcription event `transcript.text.done`. When\n * `clientDisconnected` is true AND `sawDone` is true, the stream was\n * logically complete, so the fixture SHOULD be persisted. When `sawDone` is\n * false the disconnect was a genuine mid-stream abort and the fixture MUST\n * NOT be persisted.\n */\n sawDone: boolean;\n}> {\n return new Promise((resolve, reject) => {\n const transport = target.protocol === \"https:\" ? https : http;\n const UPSTREAM_TIMEOUT_MS = clampTimeout(timeouts?.upstreamTimeoutMs, 30_000);\n const BODY_TIMEOUT_MS = clampTimeout(timeouts?.bodyTimeoutMs, 30_000);\n // Capture the moment the request is dispatched (set just before\n // `req.write` below). ttft/total durations are measured from request SEND,\n // not from when upstream HEADERS arrive — basing them on headers-received\n // time would exclude the upstream's time-to-first-byte and understate the\n // recorded time-to-first-token.\n let requestSendTime = 0;\n const req = transport.request(\n target,\n {\n method,\n timeout: UPSTREAM_TIMEOUT_MS,\n headers: {\n ...headers,\n \"Content-Length\": Buffer.byteLength(body).toString(),\n },\n },\n (res) => {\n res.setTimeout(BODY_TIMEOUT_MS, () => {\n req.destroy(new Error(`Upstream response timed out after ${BODY_TIMEOUT_MS / 1000}s`));\n });\n // Detect streaming content types so we can tee upstream chunks to the\n // client as they arrive rather than buffering the entire stream and\n // replaying it in a single res.end() at the bottom of proxyAndRecord.\n // Buffering collapses every frame into one client-visible write,\n // which defeats progressive rendering in downstream consumers and\n // can trip HTTP idle timeouts on slow calls.\n const ct = res.headers[\"content-type\"];\n const ctStr = pickContentType(ct);\n const ctLower = ctStr.toLowerCase();\n const isSSE = ctLower.includes(\"text/event-stream\");\n const isNDJSON = ctLower.includes(\"application/x-ndjson\");\n const isBinaryEventStream = ctLower.includes(\"application/vnd.amazon.eventstream\");\n const isProgressiveStream = isSSE || isNDJSON || isBinaryEventStream;\n // SSE/NDJSON frame timing capture — timestamps each complete frame\n // so proxyAndRecord can build RecordedTimings for the fixture.\n const frameTimestamps: number[] = [];\n // Measure from request send (see requestSendTime above), falling back to\n // now only if it was somehow not set before the response callback fired.\n const streamStartTime = requestSendTime || Date.now();\n let frameBuffer = \"\";\n // Decode chunks through a streaming-aware decoder so a multibyte UTF-8\n // character split across a TCP chunk boundary buffers across chunks\n // instead of decoding to U+FFFD replacement characters.\n const frameDecoder = new StreamingFrameDecoder();\n let binaryFrameBuffer = Buffer.alloc(0);\n\n let streamedToClient = false;\n let clientDisconnected = false;\n // A terminal can be legacy `[DONE]` or typed `transcript.text.done`.\n // Either means a client close before upstream `res.end` is complete.\n let sawDone = false;\n if (isProgressiveStream && clientRes && !clientRes.headersSent) {\n const relayHeaders: Record<string, string> = {\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\",\n };\n if (ctStr) relayHeaders[\"Content-Type\"] = ctStr;\n // Normalize status codes for the client: aimock acts as a gateway,\n // so upstream provider details should not leak.\n // Successes → 200, errors → 502 (Bad Gateway).\n const rawStatus = res.statusCode ?? 200;\n const clientStatus = rawStatus >= 200 && rawStatus < 300 ? 200 : 502;\n clientRes.writeHead(clientStatus, relayHeaders);\n // Flush headers immediately so the client starts parsing frames\n // before the first data chunk arrives.\n if (typeof clientRes.flushHeaders === \"function\") clientRes.flushHeaders();\n streamedToClient = true;\n clientRes.on(\"close\", () => {\n if (!clientRes.writableFinished) {\n clientDisconnected = true;\n if (!sawDone) {\n // Genuine mid-stream abort — tear down upstream so it stops\n // producing data, and clear all parse state so no partial\n // fixture is persisted. Mirrors the pre-#288 behavior.\n req.destroy();\n res.removeListener(\"data\", onUpstreamData);\n chunks.length = 0;\n bufferedBytes = 0;\n frameTimestamps.length = 0;\n frameBuffer = \"\";\n binaryFrameBuffer = Buffer.alloc(0);\n }\n // If sawDone is true: client closed after consuming a terminal\n // frame. The upstream is logically complete; let it run to\n // `res.end` so the full body is\n // buffered and the fixture can be persisted. The\n // `!clientDisconnected` guard inside `onUpstreamData` already\n // prevents further writes to the now-closed client socket.\n }\n });\n }\n const chunks: Buffer[] = [];\n // Bound the in-memory buffer used to collapse/journal the response so a\n // single huge proxied stream can neither spike the heap nor build a\n // string past V8's ~512 MiB limit (RangeError: Invalid string length).\n // The client relay below is independent of this buffer, so capping it\n // does NOT truncate what the client receives.\n let bufferedBytes = 0;\n let totalBytes = 0;\n let bufferTruncated = false;\n /** Which cap tripped truncation — surfaced to the caller for an accurate warning. */\n let truncationCap: \"byte\" | \"frame\" | undefined;\n // Snapshot the effective hard ceiling once for this request (honors the\n // test-only override). Bounds the non-progressive relay buffer; when the\n // body would exceed it we can neither buffer the full copy nor safely\n // relay the partial one, so the caller fails loud instead of truncating.\n const hardCeiling = effectiveHardCeiling();\n /**\n * True when a NON-progressive upstream body exceeded `hardCeiling`, so\n * the buffered `rawBuffer` is a PARTIAL copy that must NOT be relayed as\n * success. Distinct from `bufferTruncated` (which also covers the\n * under-ceiling over-soft-cap case where the full body IS retained).\n */\n let hardCeilingExceeded = false;\n // Trip truncation: mark the response over-cap so the caller skips\n // collapse/recording, and eagerly drop the accumulated PARSE state\n // (frameTimestamps + frame/binary parse buffers) which only ever feeds\n // recording. Reused by the top-of-callback byte guard AND the per-frame\n // guards inside the SSE/NDJSON and binary splitter loops, so a single\n // coalesced chunk carrying many complete frames cannot overshoot the\n // frame cap before the next data event re-checks.\n //\n // The raw `chunks` array is handled differently by stream shape:\n // - progressive streams (SSE/NDJSON/binary) are teed to the client\n // live, so the bytes are already on the wire — `chunks` is freed\n // immediately and the partial buffer is never relayed.\n // - non-progressive responses (a single non-stream body) are NOT teed;\n // the only copy the client can receive is `chunks`, so we KEEP\n // accumulating it (bounded by HARD_CEILING below) and relay it in\n // full. We still skip recording — the cap means \"don't journal\", not\n // \"don't answer the client\".\n const tripTruncation = (cap: \"byte\" | \"frame\") => {\n bufferTruncated = true;\n truncationCap = cap;\n frameTimestamps.length = 0;\n frameBuffer = \"\";\n // Intentional: the returned flush string is discarded (frameBuffer was\n // just cleared and recording is skipped), but `.end()` releases any\n // partial-multibyte bytes the StringDecoder is internally holding — so\n // this frees decoder state rather than being dead cleanup.\n frameDecoder.end();\n binaryFrameBuffer = Buffer.alloc(0);\n if (isProgressiveStream) {\n chunks.length = 0;\n bufferedBytes = 0;\n }\n // State which cap tripped accurately (do not conflate the two caps),\n // and report the relay truthfully — the client received every byte on\n // the streamed path and, post-fix, on the non-streamed path too.\n const detail =\n cap === \"byte\"\n ? `byte cap (${maxBufferBytes} bytes)`\n : `frame cap (${maxBufferFrames} frames)`;\n logger?.warn(\n `Upstream response exceeded the proxy buffer ${detail} — relaying full body to client, but skipping in-memory collapse/recording to bound memory`,\n );\n };\n const onUpstreamData = (chunk: Buffer) => {\n totalBytes += chunk.length;\n // Trip truncation on EITHER bytes OR frame count. The byte cap alone\n // never bounds `frameTimestamps` (count-indexed, not byte-sized) nor a\n // never-completing parse buffer, so a long-lived / never-ending stream\n // would otherwise grow per-frame state forever. `frameTimestamps.length`\n // is the running complete-frame count. `>=` (not `>`) so we never\n // retain MORE than `maxBufferFrames` frames — see the \"maximum N\n // frames retained\" contract on DEFAULT_MAX_PROXY_BUFFER_FRAMES.\n if (!bufferTruncated && bufferedBytes + chunk.length > maxBufferBytes) {\n tripTruncation(\"byte\");\n } else if (!bufferTruncated && frameTimestamps.length >= maxBufferFrames) {\n tripTruncation(\"frame\");\n }\n // Buffer the raw bytes. Under cap: always. Over cap on a progressive\n // stream: never (bytes are already teed live; chunks was freed). Over\n // cap on a NON-progressive response: keep buffering for the relay —\n // it has no live tee, so `chunks` is the only copy the client can get\n // — but hard-cap it at HARD_CEILING (well under V8's max string\n // length) so the eventual rawBuffer.toString() relay can never throw\n // RangeError: Invalid string length.\n if (!bufferTruncated) {\n chunks.push(chunk);\n bufferedBytes += chunk.length;\n } else if (!isProgressiveStream) {\n if (bufferedBytes + chunk.length <= hardCeiling) {\n chunks.push(chunk);\n bufferedBytes += chunk.length;\n } else {\n // The non-progressive relay copy would exceed the hard ceiling.\n // We CANNOT relay the full body (it can't be buffered safely) and\n // MUST NOT relay the partial buffer as a success — flag it so the\n // caller fails loud (502) instead of presenting a truncated 2xx.\n hardCeilingExceeded = true;\n }\n }\n\n // Capture per-frame timestamps for SSE/NDJSON streams. Gated on\n // !bufferTruncated so per-frame parse/timing state stops growing once\n // the cap trips (the byte/frame guard above already freed it).\n // TCP data events don't align with SSE frames — buffer and\n // split on the protocol delimiter to timestamp each complete frame.\n if (!bufferTruncated && (isSSE || isNDJSON)) {\n frameBuffer += frameDecoder.write(chunk);\n // Split on the protocol delimiter, tolerating CRLF line endings.\n // The SSE spec permits CRLF, and some upstreams/proxies emit\n // \\r\\n\\r\\n (SSE) or \\r\\n (NDJSON) frame boundaries. An LF-only\n // split would see the whole CRLF stream as a single frame and\n // lose per-frame timing. The last split element (a partial frame\n // tail) stays buffered, exactly as with a string delimiter.\n const delimiter = isNDJSON ? /\\r?\\n/ : /\\r?\\n\\r?\\n/;\n const parts = frameBuffer.split(delimiter);\n // All complete frames (everything except the last part which\n // may be incomplete). Enforce the frame cap PER-FRAME: a single\n // coalesced chunk can carry many complete frames, so checking only\n // at the top of the callback would let one event push them all and\n // overshoot the cap unbounded. Trip + bail mid-loop instead.\n for (let fi = 0; fi < parts.length - 1; fi++) {\n if (frameTimestamps.length >= maxBufferFrames) {\n tripTruncation(\"frame\");\n break;\n }\n const frame = parts[fi].trim();\n if (frame.length > 0) {\n frameTimestamps.push(Date.now());\n // Track typed and legacy terminal frames so the client-close\n // handler can distinguish a complete stream from an abort.\n if (isTerminalSSEFrame(frame)) sawDone = true;\n }\n }\n // Last part stays in buffer (may be incomplete). Skip when the\n // per-frame guard just tripped — tripTruncation already cleared it.\n if (!bufferTruncated) {\n frameBuffer = parts[parts.length - 1];\n }\n }\n\n // Binary EventStream frame boundary detection — parse the 4-byte\n // total-length prefix to detect complete frames without decoding\n // frame contents (CRC validation happens in stream-collapse).\n // Also gated on !bufferTruncated so binaryFrameBuffer stops growing\n // once the cap trips.\n if (!bufferTruncated && isBinaryEventStream) {\n // Count the binary parse buffer's growth toward the byte cap.\n // binaryFrameBuffer is a SECOND, parallel copy of the bytes (the\n // raw `chunks` array also holds them), so without this a\n // never-completing / malformed (`totalLen<12`) frame — which pushes\n // no frameTimestamps — would let a full second copy accumulate up\n // to the byte cap, peaking at ~2× the configured cap. Trip on the\n // COMBINED footprint so the byte cap bounds both copies together.\n // NOTE: `bufferedBytes` ALREADY includes the current `chunk.length`\n // (added in the raw-buffer accumulation above), so the combined\n // footprint is `bufferedBytes + binaryFrameBuffer.length`. Adding\n // `chunk.length` again would double-count it and trip the cap one\n // chunk early (matching the L<byte-guard> accounting at the top).\n if (bufferedBytes + binaryFrameBuffer.length > maxBufferBytes) {\n tripTruncation(\"byte\");\n } else {\n binaryFrameBuffer = Buffer.concat([binaryFrameBuffer, chunk]);\n while (binaryFrameBuffer.length >= 4) {\n if (frameTimestamps.length >= maxBufferFrames) {\n // Per-frame cap: a single chunk can complete many binary\n // frames; trip mid-loop rather than overshoot.\n tripTruncation(\"frame\");\n break;\n }\n const totalLen = binaryFrameBuffer.readUInt32BE(0);\n if (totalLen < 12 || binaryFrameBuffer.length < totalLen) break;\n frameTimestamps.push(Date.now());\n binaryFrameBuffer = binaryFrameBuffer.subarray(totalLen);\n }\n }\n }\n\n if (\n streamedToClient &&\n clientRes &&\n !clientDisconnected &&\n !clientRes.destroyed &&\n !clientRes.writableEnded\n ) {\n try {\n clientRes.write(chunk);\n } catch (writeErr) {\n logger?.debug(\n `Failed to relay chunk to client: ${writeErr instanceof Error ? writeErr.message : \"unknown\"}`,\n );\n clientDisconnected = true;\n }\n }\n };\n res.on(\"data\", onUpstreamData);\n res.on(\"error\", reject);\n res.on(\"end\", () => {\n if (res.socket) res.setTimeout(0);\n // Flush remaining text frame buffer — captures the last frame if\n // the stream ended without a trailing delimiter. Binary EventStream\n // frames are length-prefixed so partial frames at end-of-stream are\n // genuinely incomplete and should not be timestamped. Skipped when\n // truncated: the decoder/parse buffer were already drained+cleared on\n // the trip, and recording is skipped, so there is nothing to flush.\n if (!bufferTruncated && (isSSE || isNDJSON)) {\n // Drain any bytes the decoder buffered for an incomplete multibyte\n // sequence so the final frame text is complete before we test it.\n frameBuffer += frameDecoder.end();\n if (frameBuffer.trim().length > 0) {\n frameTimestamps.push(Date.now());\n }\n }\n const rawBuffer = Buffer.concat(chunks);\n if (\n streamedToClient &&\n clientRes &&\n !clientDisconnected &&\n !clientRes.destroyed &&\n !clientRes.writableEnded\n ) {\n try {\n clientRes.end();\n } catch (endErr) {\n logger?.debug(\n `Failed to end client response: ${endErr instanceof Error ? endErr.message : \"unknown\"}`,\n );\n }\n }\n // Decide the string `body`:\n // - not truncated: stringify the full buffer as usual.\n // - truncated PROGRESSIVE stream: `chunks` was freed on the trip, so\n // rawBuffer is empty; skip toString to keep the path allocation-free\n // (the client already got every byte via the live tee).\n // - truncated NON-progressive response: we deliberately kept the full\n // bytes (bounded by HARD_CEILING) so they can be relayed — stringify\n // them so proxyAndRecord can `res.end(body)` the real response.\n const bodyString =\n !bufferTruncated || (bufferTruncated && !streamedToClient) ? rawBuffer.toString() : \"\";\n resolve({\n status: res.statusCode ?? 500,\n headers: res.headers,\n body: bodyString,\n rawBuffer,\n streamedToClient,\n clientDisconnected,\n frameTimestamps,\n streamStartTime,\n bufferTruncated,\n truncationCap,\n totalBytes,\n hardCeilingExceeded,\n sawDone,\n });\n });\n },\n );\n req.on(\"timeout\", () => {\n req.destroy(\n new Error(\n `Upstream request timed out after ${UPSTREAM_TIMEOUT_MS / 1000}s: ${target.href}`,\n ),\n );\n });\n req.on(\"error\", reject);\n // Stamp the send time immediately before dispatching the body so ttft and\n // total-duration are measured from request send, not headers-received.\n requestSendTime = Date.now();\n req.write(body);\n req.end();\n });\n}\n\n/**\n * A captured Anthropic thinking-block signature is only persisted alongside\n * non-empty plaintext `reasoning` (a bare signature has nothing to attach to on\n * replay). When that gate drops a present signature, warn so the loss is\n * observable (matching the recording-side anomaly convention in this file).\n */\nfunction logDroppedReasoningSignature(\n logger: Logger | undefined,\n reasoning: string | undefined,\n reasoningSignature: string | undefined,\n): void {\n if (reasoningSignature && !reasoning) {\n logger?.warn(\"Dropping captured reasoningSignature — no plaintext reasoning to attach it to\");\n }\n}\n\n/** Sanitize a collapsed function call before it is persisted: string `name` and `arguments`. */\nfunction sanitizeRecordedToolCall(tc: ToolCall): ToolCall {\n return { ...tc, name: tc.name ?? \"\", arguments: tc.arguments ?? \"{}\" };\n}\n\n/**\n * Coerce a tool call's `arguments` field into the string the fixture contract\n * requires (types.ts `ToolCall.arguments: string`). When the upstream omits the\n * arguments field entirely, `JSON.stringify(undefined)` yields the JS value\n * `undefined` (not a string), which then gets dropped on disk-write and breaks\n * cross-provider replay through the OpenAI streaming path. Coalesce to \"{}\" so\n * `arguments` is ALWAYS a string, matching the streaming recorder's behavior.\n */\nfunction toToolCallArguments(raw: unknown): string {\n if (typeof raw === \"string\") return raw;\n if (raw === undefined || raw === null) return \"{}\";\n return JSON.stringify(raw);\n}\n\n/**\n * `usage` sub-objects that carry numeric breakdowns rather than a scalar. The\n * inner fields are NOT allowlisted here on purpose: the recorder keeps every\n * finite-numeric inner field (documented or forward-compat), matching the\n * load-time validator in fixture-loader.ts, whose index-signature escape hatch\n * accepts undocumented numeric inner keys too. A `Set` (own-key membership via\n * `.has()`, no prototype-chain walk) mirrors that validator's\n * `new Set(Object.keys(...))` classification so anything this recorder writes\n * loads cleanly.\n */\nconst USAGE_OBJECT_FIELDS: ReadonlySet<string> = new Set([\n \"cost_details\",\n \"prompt_tokens_details\",\n \"completion_tokens_details\",\n]);\n\n/**\n * Sanitize a provider-reported `usage` object into the fixture\n * `response.usage` override shape (#368).\n *\n * The recorder persists real upstream usage so replay can serve the provider's\n * ACTUAL token counts (instead of the `ceil(len/4)` estimate) and OpenRouter's\n * `cost` — which previously never survived recording at all. Upstream usage is\n * provider-specific and forward-extending, so this passes fields through rather\n * than whitelisting them, but only in the shapes the fixture contract accepts:\n *\n * - numeric scalars (`prompt_tokens`, `cost`, `native_tokens_prompt`, …) — kept\n * - `is_byok` boolean — kept\n * - the documented breakdown objects — kept, with non-numeric inner fields\n * dropped (the load-time validator rejects those, and a fixture the recorder\n * writes must always load)\n * - anything else (strings, arrays, nulls, unknown objects) — DROPPED, because\n * `fixture-loader.ts` errors on a non-numeric extra `usage` key and a recorded\n * fixture that fails validation is worse than one missing an exotic field\n *\n * Returns `undefined` when nothing survives, so the caller omits `usage`\n * entirely and keeps pre-#368 fixtures byte-identical.\n */\nexport function sanitizeRecordedUsage(raw: unknown): ResponseOverrides[\"usage\"] | undefined {\n if (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) return undefined;\n const out: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(raw as Record<string, unknown>)) {\n if (val === undefined || val === null) continue;\n if (USAGE_OBJECT_FIELDS.has(key)) {\n if (typeof val !== \"object\" || val === null || Array.isArray(val)) continue;\n const inner: Record<string, unknown> = {};\n for (const [innerKey, innerVal] of Object.entries(val as Record<string, unknown>)) {\n // Documented inner fields must be numeric (validator-enforced); undocumented\n // ones are unvalidated, so pass them through only when numeric too — keeping\n // the recorded object uniformly safe to re-load.\n if (typeof innerVal === \"number\" && Number.isFinite(innerVal)) inner[innerKey] = innerVal;\n }\n if (Object.keys(inner).length > 0) out[key] = inner;\n continue;\n }\n if (key === \"is_byok\") {\n if (typeof val === \"boolean\") out[key] = val;\n continue;\n }\n if (typeof val === \"number\" && Number.isFinite(val)) out[key] = val;\n }\n return Object.keys(out).length > 0 ? (out as ResponseOverrides[\"usage\"]) : undefined;\n}\n\n/**\n * Detect the response format from the parsed upstream JSON and convert\n * it into an aimock FixtureResponse.\n */\nfunction buildFixtureResponse(\n parsed: unknown,\n status: number,\n logger?: Logger,\n request?: ChatCompletionRequest,\n): FixtureResponse {\n if (parsed === null || parsed === undefined) {\n // Raw / unparseable response — save as error\n return {\n error: { message: \"Upstream returned non-JSON response\", type: \"proxy_error\" },\n status,\n };\n }\n\n const obj = parsed as Record<string, unknown>;\n\n // Error response — only match the actual { error: { message: \"...\" } } shape\n // used by OpenAI/Anthropic/etc., not arbitrary truthy `.error` fields.\n if (\n typeof obj.error === \"object\" &&\n obj.error !== null &&\n typeof (obj.error as Record<string, unknown>).message === \"string\"\n ) {\n const err = obj.error as Record<string, unknown>;\n return {\n error: {\n message: String(err.message ?? \"Unknown error\"),\n type: String(err.type ?? \"api_error\"),\n code: err.code ? String(err.code) : undefined,\n },\n status,\n };\n }\n\n // OpenAI embeddings: { data: [{ embedding: [...] }] }\n if (Array.isArray(obj.data) && obj.data.length > 0) {\n const first = obj.data[0] as Record<string, unknown>;\n if (Array.isArray(first.embedding)) {\n return { embedding: first.embedding as number[] };\n }\n // A string embedding is a base64-packed Float32 array. Decode it regardless\n // of whether the request echoed `encoding_format: \"base64\"` — some providers\n // return base64 without the client having asked for it, and gating on the\n // request echo silently drops a valid embedding into the error fixture.\n if (typeof first.embedding === \"string\") {\n const buf = Buffer.from(first.embedding, \"base64\");\n if (buf.byteLength === 0 || buf.byteLength % 4 !== 0) {\n // Malformed base64 (not a whole number of Float32s). Don't silently\n // return a valid-looking zero-dimension embedding — log it and fall\n // through to the generic error fixture so the loss is diagnosable.\n logger?.warn(\n `Could not decode base64 embedding (byteLength=${buf.byteLength} is not a positive multiple of 4) — saving as error fixture`,\n );\n } else {\n // Uint8Array constructor copies Buffer data to a fresh ArrayBuffer at offset 0,\n // guaranteeing the alignment Float32Array requires.\n const copied = new Uint8Array(buf);\n const floats = new Float32Array(copied.buffer, 0, buf.byteLength / 4);\n return { embedding: Array.from(floats) };\n }\n }\n // OpenAI image generation: { created, data: [{ url, b64_json, revised_prompt }] }\n // Enter the branch when ANY item carries media — not just data[0]. A batch\n // whose first element lacks both url and b64_json (e.g. a partial/placeholder\n // entry) but whose later element HAS one would otherwise skip the branch and\n // fall through to the error fixture, silently dropping every captured image.\n if ((obj.data as Array<Record<string, unknown>>).some((item) => item.url || item.b64_json)) {\n // Map only items that actually carry media (url or b64_json). A later item\n // lacking both — including one whose `b64_json` is an empty string — would\n // otherwise produce an empty {} image entry (all the conditional spreads\n // are skipped) — silent fidelity loss.\n const dataItems = obj.data as Array<Record<string, unknown>>;\n const images = dataItems\n .filter((item) => item.url || item.b64_json)\n .map((item) => ({\n ...(item.url ? { url: String(item.url) } : {}),\n ...(item.b64_json ? { b64Json: String(item.b64_json) } : {}),\n ...(item.revised_prompt ? { revisedPrompt: String(item.revised_prompt) } : {}),\n }));\n // Surface any dropped items (e.g. empty-string b64_json placeholders) so\n // the fidelity loss is diagnosable rather than silent.\n const droppedCount = dataItems.length - images.length;\n if (droppedCount > 0) {\n logger?.warn(\n `Dropped ${droppedCount} image item(s) from batch lacking a non-empty url or b64_json`,\n );\n }\n if (images.length === 1) {\n return { image: images[0] };\n }\n return { images };\n }\n }\n\n // Gemini Imagen: { predictions: [...] }\n if (Array.isArray(obj.predictions)) {\n const images = (obj.predictions as Array<Record<string, unknown>>).map((p) => ({\n ...(p.bytesBase64Encoded ? { b64Json: String(p.bytesBase64Encoded) } : {}),\n ...(p.mimeType ? { mimeType: String(p.mimeType) } : {}),\n }));\n if (images.length === 1) {\n return { image: images[0] };\n }\n return { images };\n }\n\n // OpenAI transcription: { text: \"...\", ... }. Modern gpt-transcribe\n // responses may be the minimal { text, languages?, usage? } shape, so trust\n // that shape only on an audio transcription/translation request. Other\n // endpoints still need the legacy markers to avoid misclassifying text APIs.\n const isTranscriptionRequest =\n request?._endpointType === \"transcription\" || request?._endpointType === \"translation\";\n const looksLikeTranscription =\n typeof obj.text === \"string\" &&\n (isTranscriptionRequest ||\n obj.task === \"transcribe\" ||\n (obj.language !== undefined && obj.duration !== undefined)) &&\n !(\"choices\" in obj) &&\n !(\"candidates\" in obj) &&\n !(\"object\" in obj) &&\n !(\"message\" in obj) &&\n !(\"outputs\" in obj);\n if (looksLikeTranscription) {\n return {\n transcription: {\n text: obj.text as string,\n ...(obj.language ? { language: String(obj.language) } : {}),\n ...(Array.isArray(obj.languages)\n ? {\n languages: obj.languages\n .filter(\n (language): language is Record<string, unknown> =>\n typeof language === \"object\" &&\n language !== null &&\n typeof language.code === \"string\",\n )\n .map((language) => ({ code: language.code as string })),\n }\n : {}),\n ...(obj.duration !== undefined ? { duration: Number(obj.duration) } : {}),\n ...(obj.usage && typeof obj.usage === \"object\"\n ? { usage: obj.usage as Record<string, unknown> }\n : {}),\n ...(Array.isArray(obj.words) ? { words: obj.words } : {}),\n ...(Array.isArray(obj.segments) ? { segments: obj.segments } : {}),\n },\n };\n }\n\n // Gemini Interactions: { id, status, outputs: [{ type: \"text\", text }, { type: \"function_call\", name, arguments }] }\n if (\n Array.isArray(obj.outputs) &&\n obj.outputs.length > 0 &&\n !(\"choices\" in obj) &&\n !(\"content\" in obj) &&\n !(\"candidates\" in obj)\n ) {\n const outputs = obj.outputs as Array<Record<string, unknown>>;\n const fnCallOutputs = outputs.filter((o) => o.type === \"function_call\");\n const textOutputs = outputs.filter((o) => o.type === \"text\" && typeof o.text === \"string\");\n const hasToolCalls = fnCallOutputs.length > 0;\n const joinedText = textOutputs.map((o) => String(o.text ?? \"\")).join(\"\");\n const hasContent = joinedText.length > 0;\n\n if (hasToolCalls) {\n const toolCalls: ToolCall[] = fnCallOutputs.map((o) => ({\n name: String(o.name),\n arguments: toToolCallArguments(o.arguments),\n ...(o.id ? { id: String(o.id) } : {}),\n }));\n if (hasContent) {\n return { content: joinedText, toolCalls };\n }\n return { toolCalls };\n }\n if (hasContent) {\n return { content: joinedText };\n }\n // Recognized Gemini Interactions shape but empty content\n return { content: \"\" };\n }\n\n // OpenAI video generation: { id, status, ... }\n // Guard against false positives: many API responses have `id` + `status` fields\n // (e.g. chat completions, Anthropic messages). Reject if the response has fields\n // that indicate a known non-video format.\n if (\n typeof obj.id === \"string\" &&\n typeof obj.status === \"string\" &&\n (obj.status === \"completed\" || obj.status === \"in_progress\" || obj.status === \"failed\") &&\n !(\"choices\" in obj) &&\n !(\"content\" in obj) &&\n !(\"candidates\" in obj) &&\n !(\"message\" in obj) &&\n !(\"data\" in obj) &&\n !(\"object\" in obj) &&\n !(\"outputs\" in obj) &&\n !(\"model\" in obj) &&\n !(\"response\" in obj) &&\n !(\"done\" in obj) &&\n !(\"usage\" in obj) &&\n !(\"error\" in obj)\n ) {\n if (obj.status === \"completed\" && obj.url) {\n return {\n video: {\n id: String(obj.id),\n status: \"completed\" as const,\n url: String(obj.url),\n },\n };\n }\n return {\n video: {\n id: String(obj.id),\n status: obj.status === \"failed\" ? (\"failed\" as const) : (\"processing\" as const),\n },\n };\n }\n\n // Direct embedding: { embedding: [...] }\n if (Array.isArray(obj.embedding)) {\n return { embedding: obj.embedding as number[] };\n }\n\n // OpenAI chat completion: { choices: [{ message: { content, tool_calls } }] }\n if (Array.isArray(obj.choices) && obj.choices.length > 0) {\n const choice = obj.choices[0] as Record<string, unknown>;\n const message = choice.message as Record<string, unknown> | undefined;\n if (message) {\n const hasToolCalls = Array.isArray(message.tool_calls) && message.tool_calls.length > 0;\n const hasContent = typeof message.content === \"string\" && message.content.length > 0;\n\n // Reasoning is exposed under different keys across OpenAI-compatible\n // providers: OpenAI/vLLM use `reasoning_content`, while DeepSeek and\n // OpenRouter use `reasoning`. Read both (preferring `reasoning_content`)\n // so a reasoning turn is not silently dropped on the latter providers.\n const openaiReasoning =\n typeof message.reasoning_content === \"string\" && message.reasoning_content.length > 0\n ? message.reasoning_content\n : typeof message.reasoning === \"string\" && message.reasoning.length > 0\n ? message.reasoning\n : undefined;\n\n // Provider-reported usage from the non-streaming envelope (#368), at\n // parity with the collapsed-stream path (both capture usage for\n // OpenAI-compatible providers only — only `collapseOpenAISSE` sets\n // `CollapseResult.usage`): replay then serves the real token counts rather\n // than the `ceil(len/4)` estimate, and OpenRouter's `usage.cost` survives\n // recording. Omitted when upstream sent no usage.\n const openaiUsage = sanitizeRecordedUsage(obj.usage);\n const usageSpread = openaiUsage ? { usage: openaiUsage } : {};\n\n if (hasToolCalls) {\n const toolCalls: ToolCall[] = (message.tool_calls as Array<Record<string, unknown>>).map(\n (tc) => {\n const fn = tc.function as Record<string, unknown>;\n return {\n name: String(fn.name),\n arguments: toToolCallArguments(fn.arguments),\n ...(tc.id ? { id: String(tc.id) } : {}),\n };\n },\n );\n if (hasContent) {\n return {\n content: message.content as string,\n toolCalls,\n ...(openaiReasoning ? { reasoning: openaiReasoning } : {}),\n ...usageSpread,\n };\n }\n return {\n toolCalls,\n ...(openaiReasoning ? { reasoning: openaiReasoning } : {}),\n ...usageSpread,\n };\n }\n // Text content only\n if (hasContent) {\n return {\n content: message.content as string,\n ...(openaiReasoning ? { reasoning: openaiReasoning } : {}),\n ...usageSpread,\n };\n }\n // Recognized OpenAI shape but empty content (e.g. content filtering, zero max_tokens)\n return {\n content: \"\",\n ...(openaiReasoning ? { reasoning: openaiReasoning } : {}),\n ...usageSpread,\n };\n }\n }\n\n // Anthropic: { content: [{ type: \"text\", text: \"...\" }] } or tool_use\n if (Array.isArray(obj.content) && obj.content.length > 0) {\n const blocks = obj.content as Array<Record<string, unknown>>;\n const toolUseBlocks = blocks.filter((b) => b.type === \"tool_use\");\n const textBlocks = blocks.filter((b) => b.type === \"text\" && typeof b.text === \"string\");\n const thinkingBlocks = blocks.filter((b) => b.type === \"thinking\");\n // Raw `redacted_thinking` block presence drives reasoning-bearing\n // classification below (mirrors how `thinkingBlocks` keys on PRESENCE, not\n // surviving payload). A turn whose redacted blocks all carry empty `data`\n // is constructible upstream and must NOT fall through to the error fallback.\n const redactedBlocks = blocks.filter((b) => b.type === \"redacted_thinking\");\n // A `redacted_thinking` block carries its encrypted reasoning in an opaque\n // `data` string; collect the SURVIVING (non-empty) payloads in content-array\n // order for the persisted fixture so the recorded turn round-trips its\n // redacted blocks (mirrors the streaming collapse path; see\n // capturedRedactedData for the non-empty rule — empty-data blocks are dropped\n // from the payload, but still count toward classification via redactedBlocks).\n const redactedThinking = blocks\n .map((b) => capturedRedactedData(b))\n .filter((data): data is string => data !== undefined);\n const hasToolCalls = toolUseBlocks.length > 0;\n const joinedText = textBlocks.map((b) => String(b.text ?? \"\")).join(\"\");\n const hasContent = joinedText.length > 0;\n const anthropicReasoning =\n thinkingBlocks.length > 0\n ? thinkingBlocks.map((b) => String(b.thinking ?? \"\")).join(\"\")\n : undefined;\n // The real cryptographic signature is carried only when reasoning is also\n // present (a bare signature has nothing to attach to on replay), matching the\n // streaming recorder's gating. Multi-thinking-block parity: collapseAnthropicSSE\n // overwrites reasoningSignature on every signature_delta (last-signature-wins),\n // and a thinking block that streams NO signature_delta leaves the prior value\n // intact. Mirror both: take the LAST block that actually carries a signature, so\n // a block missing one does not clobber an earlier signature.\n const anthropicReasoningSignature = (() => {\n let sig: string | undefined;\n for (const b of thinkingBlocks) {\n if (typeof b.signature === \"string\") sig = String(b.signature);\n }\n return sig;\n })();\n // Carry the real Anthropic thinking-block signature only when reasoning is\n // also present; redacted blocks carry their OWN encrypted reasoning so they\n // are carried independently of any plaintext `reasoning`. Both mirror the\n // streaming spread gating in proxyAndRecord.\n const reasoningSignatureSpread =\n anthropicReasoning && anthropicReasoningSignature\n ? { reasoningSignature: anthropicReasoningSignature }\n : {};\n logDroppedReasoningSignature(logger, anthropicReasoning, anthropicReasoningSignature);\n const redactedThinkingSpread = redactedThinking.length > 0 ? { redactedThinking } : {};\n\n if (hasToolCalls) {\n const toolCalls: ToolCall[] = toolUseBlocks.map((b) => ({\n name: String(b.name),\n arguments: toToolCallArguments(b.input),\n ...(b.id ? { id: String(b.id) } : {}),\n }));\n if (hasContent) {\n return {\n content: joinedText,\n toolCalls,\n ...(anthropicReasoning ? { reasoning: anthropicReasoning } : {}),\n ...reasoningSignatureSpread,\n ...redactedThinkingSpread,\n };\n }\n return {\n toolCalls,\n ...(anthropicReasoning ? { reasoning: anthropicReasoning } : {}),\n ...reasoningSignatureSpread,\n ...redactedThinkingSpread,\n };\n }\n if (hasContent) {\n return {\n content: joinedText,\n ...(anthropicReasoning ? { reasoning: anthropicReasoning } : {}),\n ...reasoningSignatureSpread,\n ...redactedThinkingSpread,\n };\n }\n // Thinking-only / redacted-only response (no text, no tool calls). A turn can\n // carry only thinking blocks (even ones whose plaintext is empty but which\n // bear a real signature) or only redacted_thinking blocks (even ones whose\n // `data` is empty and thus dropped from the persisted payload), so key on the\n // PRESENCE of those RAW blocks — not the truthiness of the joined thinking text\n // nor the post-filter `redactedThinking` array — and produce a normal\n // empty-content fixture rather than falling through to the error fallback\n // below. This matches the streaming path, which classifies on content\n // emptiness for the same logical turn. (Per the persistence contract, a bare\n // signature with empty reasoning is still dropped via\n // logDroppedReasoningSignature, and empty-data redacted blocks yield NO\n // redactedThinking field.)\n if (thinkingBlocks.length > 0 || redactedBlocks.length > 0) {\n return {\n content: \"\",\n ...(anthropicReasoning ? { reasoning: anthropicReasoning } : {}),\n ...reasoningSignatureSpread,\n ...redactedThinkingSpread,\n };\n }\n }\n\n // Gemini: { candidates: [{ content: { parts: [{ text: \"...\" }] } }] }\n if (Array.isArray(obj.candidates) && obj.candidates.length > 0) {\n const candidate = obj.candidates[0] as Record<string, unknown>;\n const content = candidate.content as Record<string, unknown> | undefined;\n if (content && Array.isArray(content.parts)) {\n const parts = content.parts as Array<Record<string, unknown>>;\n\n // Audio inlineData parts take priority over text. Key on the PRESENCE of\n // `inlineData.data` rather than solely on the mimeType: an audio part can\n // arrive with a missing or unexpected mimeType, and filtering strictly on\n // an `audio/` prefix would then drop a part that genuinely carries audio\n // bytes, producing an empty b64Json. A NON-audio mimeType (e.g. `image/`)\n // still routes elsewhere, so image inlineData is not misclassified — only\n // an explicit `audio/` prefix OR a missing/blank mimeType counts as audio.\n const audioParts = parts.filter((p: Record<string, unknown>) => {\n const inline = p.inlineData as Record<string, unknown> | undefined;\n if (\n inline === undefined ||\n inline === null ||\n typeof inline.data !== \"string\" ||\n inline.data.length === 0\n ) {\n return false;\n }\n const mt = inline.mimeType;\n if (typeof mt === \"string\" && mt.length > 0) {\n return mt.startsWith(\"audio/\");\n }\n // Missing/blank mimeType but data present → treat as audio.\n return true;\n });\n if (audioParts.length > 0) {\n const inlineData = audioParts[0].inlineData as Record<string, unknown>;\n return {\n audio: {\n b64Json: String(inlineData.data ?? \"\"),\n contentType:\n typeof inlineData.mimeType === \"string\" && inlineData.mimeType.length > 0\n ? inlineData.mimeType\n : \"audio/mpeg\",\n },\n };\n }\n\n const fnCallParts = parts.filter((p) => p.functionCall);\n const textParts = parts.filter((p) => typeof p.text === \"string\" && !p.thought);\n const thoughtParts = parts.filter((p) => p.thought === true && typeof p.text === \"string\");\n const hasToolCalls = fnCallParts.length > 0;\n const joinedText = textParts.map((p) => String(p.text ?? \"\")).join(\"\");\n const hasContent = joinedText.length > 0;\n const geminiReasoning =\n thoughtParts.length > 0\n ? thoughtParts.map((p) => String(p.text ?? \"\")).join(\"\")\n : undefined;\n\n if (hasToolCalls) {\n const toolCalls: ToolCall[] = fnCallParts.map((p) => {\n const fc = p.functionCall as Record<string, unknown>;\n return {\n name: String(fc.name),\n arguments: toToolCallArguments(fc.args),\n };\n });\n if (hasContent) {\n return {\n content: joinedText,\n toolCalls,\n ...(geminiReasoning ? { reasoning: geminiReasoning } : {}),\n };\n }\n return { toolCalls, ...(geminiReasoning ? { reasoning: geminiReasoning } : {}) };\n }\n if (hasContent) {\n return {\n content: joinedText,\n ...(geminiReasoning ? { reasoning: geminiReasoning } : {}),\n };\n }\n // Recognized Gemini shape but empty content\n return { content: \"\", ...(geminiReasoning ? { reasoning: geminiReasoning } : {}) };\n }\n }\n\n // Bedrock Converse: { output: { message: { role, content: [{ text }, { toolUse }] } } }\n if (obj.output && typeof obj.output === \"object\") {\n const output = obj.output as Record<string, unknown>;\n const msg = output.message as Record<string, unknown> | undefined;\n if (msg && Array.isArray(msg.content)) {\n const blocks = msg.content as Array<Record<string, unknown>>;\n const toolUseBlocks = blocks.filter((b) => b.toolUse);\n const textBlocks = blocks.filter((b) => typeof b.text === \"string\");\n const reasoningBlocks = blocks.filter((b) => b.reasoningContent);\n const hasToolCalls = toolUseBlocks.length > 0;\n const joinedText = textBlocks.map((b) => String(b.text ?? \"\")).join(\"\");\n const hasContent = joinedText.length > 0;\n const bedrockReasoning =\n reasoningBlocks.length > 0\n ? reasoningBlocks\n .map((b) => {\n const rc = b.reasoningContent as Record<string, unknown>;\n const rt = rc?.reasoningText as Record<string, unknown> | undefined;\n return String(rt?.text ?? \"\");\n })\n .join(\"\")\n : undefined;\n\n if (hasToolCalls) {\n const toolCalls: ToolCall[] = toolUseBlocks.map((b) => {\n const tu = b.toolUse as Record<string, unknown>;\n return {\n name: String(tu.name ?? \"\"),\n arguments: toToolCallArguments(tu.input),\n ...(tu.toolUseId ? { id: String(tu.toolUseId) } : {}),\n };\n });\n if (hasContent) {\n return {\n content: joinedText,\n toolCalls,\n ...(bedrockReasoning ? { reasoning: bedrockReasoning } : {}),\n };\n }\n return { toolCalls, ...(bedrockReasoning ? { reasoning: bedrockReasoning } : {}) };\n }\n if (hasContent) {\n return {\n content: joinedText,\n ...(bedrockReasoning ? { reasoning: bedrockReasoning } : {}),\n };\n }\n // Recognized Bedrock Converse shape but empty content\n return { content: \"\", ...(bedrockReasoning ? { reasoning: bedrockReasoning } : {}) };\n }\n }\n\n // Cohere v2 chat: { finish_reason: \"...\", message: { content: [{ type: \"text\", text: \"...\" }] } }\n // Must come before Ollama since both have `message`, but Cohere has `finish_reason` at top level\n // (not nested in `choices`) and `message.content` as an array of typed objects.\n if (\n typeof obj.finish_reason === \"string\" &&\n obj.message &&\n typeof obj.message === \"object\" &&\n Array.isArray((obj.message as Record<string, unknown>).content)\n ) {\n const msg = obj.message as Record<string, unknown>;\n const contentBlocks = msg.content as Array<Record<string, unknown>>;\n // Join ALL text blocks, not just the first — a multi-block Cohere response\n // would otherwise be truncated to its leading segment.\n const joinedText = contentBlocks\n .filter((b) => b.type === \"text\" && typeof b.text === \"string\")\n .map((b) => String(b.text ?? \"\"))\n .join(\"\");\n const hasContent = joinedText.length > 0;\n const toolCallBlocks = contentBlocks.filter((b) => b.type === \"tool_call\");\n\n // Also check message-level tool_calls (Cohere v2 puts tool calls here, not in content blocks)\n const msgToolCalls = Array.isArray(msg.tool_calls)\n ? (msg.tool_calls as Array<Record<string, unknown>>)\n : [];\n\n if (toolCallBlocks.length > 0) {\n const toolCalls: ToolCall[] = toolCallBlocks.map((b) => ({\n name: String(b.name ?? (b.function as Record<string, unknown>)?.name ?? \"\"),\n arguments:\n typeof b.parameters === \"string\"\n ? b.parameters\n : typeof b.parameters === \"object\"\n ? JSON.stringify(b.parameters)\n : typeof (b.function as Record<string, unknown>)?.arguments === \"string\"\n ? String((b.function as Record<string, unknown>).arguments)\n : toToolCallArguments((b.function as Record<string, unknown>)?.arguments),\n ...(b.id ? { id: String(b.id) } : {}),\n }));\n if (hasContent) {\n return { content: joinedText, toolCalls };\n }\n return { toolCalls };\n }\n if (msgToolCalls.length > 0) {\n const toolCalls: ToolCall[] = msgToolCalls.map((tc) => {\n const fn = tc.function as Record<string, unknown> | undefined;\n return {\n name: String(tc.name ?? fn?.name ?? \"\"),\n arguments:\n typeof tc.parameters === \"string\"\n ? tc.parameters\n : typeof tc.parameters === \"object\"\n ? JSON.stringify(tc.parameters)\n : typeof fn?.arguments === \"string\"\n ? String(fn.arguments)\n : toToolCallArguments(fn?.arguments),\n ...(tc.id ? { id: String(tc.id) } : {}),\n };\n });\n if (hasContent) {\n return { content: joinedText, toolCalls };\n }\n return { toolCalls };\n }\n if (hasContent) {\n return { content: joinedText };\n }\n // Recognized Cohere v2 shape (finish_reason + message.content array) with\n // no text and no tool calls. Own the empty turn here and return an\n // empty-content fixture rather than falling through to the Ollama branch\n // below, which would re-handle `message.content` and mis-route it.\n return { content: \"\" };\n }\n\n // Ollama: { message: { content: \"...\", tool_calls: [...] } }\n if (obj.message && typeof obj.message === \"object\") {\n const msg = obj.message as Record<string, unknown>;\n const hasOllamaToolCalls = Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0;\n const hasOllamaContent = typeof msg.content === \"string\" && msg.content.length > 0;\n\n if (hasOllamaToolCalls) {\n const toolCalls: ToolCall[] = (msg.tool_calls as Array<Record<string, unknown>>)\n .filter((tc) => tc.function != null)\n .map((tc) => {\n const fn = tc.function as Record<string, unknown>;\n return {\n name: String(fn.name ?? \"\"),\n arguments: toToolCallArguments(fn.arguments),\n };\n });\n if (hasOllamaContent) {\n return { content: msg.content as string, toolCalls };\n }\n return { toolCalls };\n }\n if (hasOllamaContent) {\n return { content: msg.content as string };\n }\n // Ollama message with content array (like Cohere)\n if (Array.isArray(msg.content) && msg.content.length > 0) {\n const first = msg.content[0] as Record<string, unknown>;\n if (typeof first.text === \"string\") {\n return { content: first.text };\n }\n }\n }\n\n // Ollama /api/generate: { response: \"...\", done: true/false }\n // Narrowed: require `done` to be a boolean — Ollama always sends a boolean\n // here, and gating merely on `\"done\" in obj` would capture unrelated payloads\n // that happen to carry a `response` string and some other `done`-keyed value.\n if (typeof obj.response === \"string\" && typeof obj.done === \"boolean\") {\n return { content: obj.response };\n }\n\n // Fallback: unknown format — save as error. Log the observed top-level shape\n // so an unrecognized/new provider response is diagnosable instead of silently\n // becoming an opaque error fixture.\n logger?.warn(\n `Could not detect response format from upstream (status=${status}) — saving as error fixture; top-level keys: [${Object.keys(\n obj,\n ).join(\", \")}]`,\n );\n return {\n error: {\n message: \"Could not detect response format from upstream\",\n type: \"proxy_error\",\n },\n status,\n };\n}\n\n/**\n * Derive fixture match criteria from the original request.\n */\ntype EndpointType = NonNullable<FixtureMatch[\"endpoint\"]>;\n\nexport function buildFixtureMatch(\n request: ChatCompletionRequest,\n recordConfig?: RecordConfig,\n): {\n userMessage?: string;\n inputText?: string;\n model?: string;\n endpoint?: EndpointType;\n turnIndex?: number;\n hasToolResult?: boolean;\n context?: string;\n} {\n const match: {\n userMessage?: string;\n inputText?: string;\n model?: string;\n endpoint?: EndpointType;\n turnIndex?: number;\n hasToolResult?: boolean;\n context?: string;\n } = {};\n\n // Include endpoint type for multimedia fixtures\n if (request._endpointType && request._endpointType !== \"chat\") {\n match.endpoint = request._endpointType as EndpointType;\n }\n\n // Embedding request\n if (request.embeddingInput) {\n match.inputText = request.embeddingInput;\n return match;\n }\n\n // Chat/multimedia request — match on the last user message\n const lastUser = getLastMessageByRole(request.messages ?? [], \"user\");\n if (lastUser) {\n const text = getTextContent(lastUser.content);\n if (text) {\n match.userMessage = text;\n }\n }\n\n // Record normalized model for all requests so fixtures disambiguate\n // calls that share the same userMessage but target different models.\n if (request.model) {\n match.model =\n normalizeModelName(request.model, recordConfig?.recordFullModelVersion) ?? request.model;\n }\n\n // Multi-turn disambiguation: writing only `userMessage` lets the recorder's\n // in-memory cache shadow follow-up turns that share it (initial tool call\n // vs. text reply after the tool result). turnIndex + hasToolResult give\n // each call a distinct, matcher-aware key. Skip for non-chat (no messages).\n //\n // hasToolResult MUST be stamped with the SAME current-turn predicate the\n // matcher uses ({@link currentTurnHasToolResult}) — NOT a whole-conversation\n // `messages.some(role === \"tool\")`. A whole-conversation stamp on a genuine\n // turn-2 leg-1 request (a fresh user question whose history still carries an\n // earlier turn's tool result) writes `true`, but the matcher computes the\n // turn-scoped `false` for that same request, so the fixture could never match\n // its own recorded request. Sharing the one helper is what keeps record and\n // replay symmetric.\n const messages = request.messages ?? [];\n if (\n messages.length > 0 &&\n (request._endpointType === \"chat\" || request._endpointType === undefined)\n ) {\n match.turnIndex = messages.filter((m) => m.role === \"assistant\").length;\n match.hasToolResult = currentTurnHasToolResult(messages);\n }\n\n if (request._context) {\n match.context = request._context;\n }\n\n return match;\n}\n\n/**\n * Build optional metadata for drift detection. Contains 8-char SHA-256\n * hashes of the system prompt and tool definitions present in the request.\n * Returns undefined when neither is present.\n */\nfunction buildFixtureMetadata(\n request: ChatCompletionRequest,\n): { systemHash?: string; toolsHash?: string } | undefined {\n const meta: { systemHash?: string; toolsHash?: string } = {};\n\n const messages = request.messages ?? [];\n const systemTexts = messages\n .filter((m) => m.role === \"system\")\n .map((m) => (typeof m.content === \"string\" ? m.content : JSON.stringify(m.content)))\n .join(\"\\n\");\n if (systemTexts) {\n meta.systemHash = crypto.createHash(\"sha256\").update(systemTexts).digest(\"hex\").slice(0, 8);\n }\n\n if (request.tools && request.tools.length > 0) {\n meta.toolsHash = crypto\n .createHash(\"sha256\")\n .update(JSON.stringify(request.tools))\n .digest(\"hex\")\n .slice(0, 8);\n }\n\n return Object.keys(meta).length > 0 ? meta : undefined;\n}\n\n// ─── Service fakes persistence (MCP recorder, MR11) ─────────────────────────\n\n/**\n * Outcome of `persistServiceFakes`. `failed.stage` says where it failed:\n * `read` (the existing file is not JSON or not an object), `validate` (the\n * merged file is invalid; `error` is the validator's text) or `write` (a\n * filesystem error).\n */\nexport type PersistServiceFakesResult =\n | { kind: \"written\"; filepath: string; sha256: string; content: Record<string, unknown> }\n | { kind: \"failed\"; stage: \"read\" | \"validate\" | \"write\"; error: string };\n\n/**\n * MR11: read-merge-validate-write one service fakes file atomically (temp +\n * rename, 0o600). Synchronous, so writes to one path never interleave in this\n * process. Keeps unknown top-level keys (fakes:F9). `validate` returns a\n * problem text, or null when the whole merged file is valid. Logs RL4 on a\n * write; a failure is returned, never logged here (the caller logs RL1 with\n * its own context).\n */\nexport function persistServiceFakes(opts: {\n filepath: string;\n merge: (existing: Record<string, unknown> | null) => Record<string, unknown>;\n validate: (content: Record<string, unknown>) => string | null;\n logger: Logger;\n}): PersistServiceFakesResult {\n const { filepath, merge, validate, logger } = opts;\n let existing: Record<string, unknown> | null = null;\n let text: string | null = null;\n try {\n text = fs.readFileSync(filepath, \"utf-8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n const msg = err instanceof Error ? err.message : \"Unknown filesystem error\";\n return { kind: \"failed\", stage: \"write\", error: msg };\n }\n }\n if (text !== null) {\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n return { kind: \"failed\", stage: \"read\", error: `${filepath} is not valid JSON` };\n }\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n return { kind: \"failed\", stage: \"read\", error: `${filepath} is not a JSON object` };\n }\n existing = parsed as Record<string, unknown>;\n }\n const content = merge(existing);\n const problem = validate(content);\n if (problem !== null) return { kind: \"failed\", stage: \"validate\", error: problem };\n\n let tmpPath: string | undefined;\n try {\n fs.mkdirSync(path.dirname(filepath), { recursive: true });\n const bytes = Buffer.from(JSON.stringify(content, null, 2), \"utf-8\");\n tmpPath = filepath + \".tmp.\" + crypto.randomUUID();\n fs.writeFileSync(tmpPath, bytes, { flag: \"wx\", mode: 0o600 });\n fs.renameSync(tmpPath, filepath);\n tmpPath = undefined;\n logger.warn(`Response recorded → ${filepath}`);\n const sha256 = crypto.createHash(\"sha256\").update(bytes).digest(\"hex\");\n return { kind: \"written\", filepath, sha256, content };\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown filesystem error\";\n return { kind: \"failed\", stage: \"write\", error: msg };\n } finally {\n if (tmpPath) {\n try {\n fs.unlinkSync(tmpPath);\n } catch {\n /* Preserve the original persistence error. */\n }\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAkCA,SAAS,mBAAmB,OAAwB;CAClD,MAAM,OAAO,MACV,MAAM,QAAQ,CACd,QAAQ,SAAS,KAAK,WAAW,QAAQ,CAAC,CAC1C,KAAK,SAAS,KAAK,MAAM,EAAe,CAAC,WAAW,CAAC,CACrD,KAAK,KAAK,CACV,MAAM;AAET,KAAI,SAAS,SAAU,QAAO;AAE9B,KAAI;AACF,SAAQ,KAAK,MAAM,KAAK,CAAwB,SAAS;SACnD;AACN,SAAO;;;;;;;;;;;;;;;;;;;AAoBX,MAAa,iCAAiC,KAAK,OAAO;;;;;;;;;;;;;;AAe1D,MAAa,kCAAkC;;;;;;;;;;;;;AAc/C,MAAa,4BAA4B,MAAM,OAAO;;;;;;;AAQtD,IAAI;;AAQJ,SAAS,uBAA+B;AACtC,QAAO,kCAAkC;;AAG3C,MAAM,gBAAgB,IAAI,IAAI;CAE5B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CAEA;CACA;CAIA;CACA;CACA;CACD,CAAC;;;;;;;AAQF,MAAM,4BAA4B,IAAI,IAAI,CAAC,eAAe,CAAC;;;;;;;;;AAU3D,SAAgB,oBAAoB,KAAmD;CACrF,MAAM,MAA8B,EAAE;AACtC,MAAK,MAAM,CAAC,MAAM,QAAQ,OAAO,QAAQ,IAAI,QAAQ,EAAE;EACrD,MAAM,QAAQ,KAAK,aAAa;AAChC,MACE,QAAQ,UACR,cAAc,IAAI,MAAM,IACvB,0BAA0B,IAAI,MAAM,IAAI,mBAAmB,IAAI,IAChE,MAAM,WAAW,kBAAkB,IAClC,uBAAuB,IAAI,IAAI,yBAAyB,MAAM,CAE/D;AAEF,MAAI,QAAQ,MAAM,QAAQ,IAAI,GAAG,IAAI,KAAK,KAAK,GAAG;;AAEpD,QAAO;;;AAIT,SAAgB,oBAAoB,SAAiC,MAAoB;CACvF,MAAM,YAAY,KAAK,aAAa;AACpC,MAAK,MAAM,OAAO,OAAO,KAAK,QAAQ,CACpC,KAAI,IAAI,aAAa,KAAK,UAAW,QAAO,QAAQ;;;;;;;;;;AAYxD,SAAgB,wBACd,SACA,MACA,OACM;CACN,MAAM,YAAY,KAAK,aAAa;AACpC,MAAK,MAAM,OAAO,OAAO,KAAK,QAAQ,CACpC,KAAI,IAAI,aAAa,KAAK,UAAW;AAEvC,SAAQ,QAAQ;;;;;;;AAQlB,SAAgB,qBACd,KACA,QACA,aACA,eACoC;CACpC,MAAM,UAAU,oBAAoB,IAAI;AACxC,KAAI,CAAC,uBAAuB,IAAI,EAAE;AAChC,oBAAkB,SAAS,QAAQ,aAAa,cAAc;AAC9D,SAAO;;AAET,MAAK,MAAM,QAAQ,OAAO,KAAK,QAAQ,CACrC,KAAI,yBAAyB,KAAK,CAAE,QAAO,QAAQ;AAErD,QAAO,iBAAiB,4BAA4B,SAAS,QAAQ,aAAa,cAAc,GAC5F,UACA;;;;;;;;;;AA6EN,SAAgB,oBAAoB,OAAuB;AACzD,QAAO,MAAM,QAAQ,4BAA4B,IAAI;;;;;;;;;;;AAYvD,SAAgB,gBAAgB,aAAoD;AAClF,KAAI,MAAM,QAAQ,YAAY,CAC5B,QAAO,YAAY,SAAS,IAAI,YAAY,KAAK;AAEnD,QAAO,eAAe;;;;;;;;;;AAWxB,SAAgB,eAAe,MAUN;CACvB,MAAM,EAAE,QAAQ,aAAa,QAAQ,SAAS,UAAU,WAAW,EAAE,EAAE,WAAW;CAClF,MAAM,UAAU,KAAK,cAAc;CAMnC,MAAM,IAAI,QAAQ;CAClB,MAAM,eACJ,EAAE,gBAAgB,UAAa,EAAE,cAAc,UAAa,EAAE,aAAa;AAE7E,KAAI,OAAO,WAAW;AACpB,SAAO,KAAK,WAAW,YAAY,4BAA4B;AAC/D,SAAO,EAAE,MAAM,WAAW;;AAK5B,KAAI,aACF,QAAO,KAAK,8EAA8E;CAG5F,MAAM,cAAc,OAAO,eAAe;CAC1C,IAAI,iBAAiB,WAAW;CAChC,IAAI;CACJ,IAAI,gBAAgB;AAEpB,KAAI,gBAAgB;EAClB,MAAM,OAAO,cAAc,OAAO;AAClC,MAAI,CAAC,MAAM;AAGT,oBAAiB;GACjB,MAAM,6BAAY,IAAI,MAAM,EAAC,aAAa,CAAC,QAAQ,SAAS,IAAI;AAChE,cAAW,KAAK,KACd,aACA,GAAG,QAAQ,GAAG,UAAU,GAAGA,SAAO,YAAY,CAAC,MAAM,GAAG,EAAE,CAAC,OAC5D;SACI;AACL,cAAW,KAAK,KAAK,aAAa,MAAM,GAAG,QAAQ,OAAO;AAC1D,mBAAgB;;QAEb;EAEL,MAAM,gBAAgB,GAAG,QAAQ,oBADf,IAAI,MAAM,EAAC,aAAa,CAAC,QAAQ,SAAS,IAAI,CAClB,GAAGA,SAAO,YAAY,CAAC,MAAM,GAAG,EAAE,CAAC;EAKjF,MAAM,iBAAiB,QAAQ,MAAM,UACjC,eAAe,QAAQ,MAAM,QAAQ,GACrC;AACJ,aAAW,iBACP,KAAK,KAAK,aAAa,gBAAgB,cAAc,GACrD,KAAK,KAAK,aAAa,cAAc;;CAG3C,MAAM,eAAe,CACnB,GAAI,eAAe,CAAC,iEAAiE,GAAG,EAAE,EAC1F,GAAG,SACJ;CAED,IAAI;AACJ,KAAI;AACF,KAAG,UAAU,KAAK,QAAQ,SAAS,EAAE,EAAE,WAAW,MAAM,CAAC;EAYzD,IAAI;AAMJ,MAAI,iBAAiB,GAAG,WAAW,SAAS,CAC1C,KAAI;GACF,MAAM,WAAW,KAAK,MAAM,GAAG,aAAa,UAAU,QAAQ,CAAC;GAI/D,MAAM,mBAAmB,MAAM,QAAQ,SAAS,SAAS,GACpD,SAAS,WACV;AACJ,OAAI,qBAAqB,UAAa,SAAS,aAAa,OAC1D,QAAO,KACL,yBAAyB,SAAS,gEACnC;GAMH,MAAM,WAAoC,EAAE;AAC5C,OAAI,aAAa,QAAQ,OAAO,aAAa,YAAY,CAAC,MAAM,QAAQ,SAAS,CAC/E,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,SAAoC,EAAE;AAC9E,QAAI,QAAQ,cAAc,QAAQ,eAAe,QAAQ,WAAY;AACrE,aAAS,OAAO;;AAGpB,iBAAc;IAAE,GAAG;IAAU,UAAU,CAAC,GAAI,oBAAoB,EAAE,EAAG,QAAQ;IAAE;AAQ/E,OAAI,MAAM,QAAQ,SAAS,UAAU,CACnC,cAAa,QACX,GAAI,SAAS,UAAwB,QAAQ,MAAmB,OAAO,MAAM,SAAS,CACvF;YACQ,OAAO,SAAS,aAAa,YAAY,SAAS,SAC3D,cAAa,QAAQ,GAAG,SAAS,SAAS,MAAM,KAAK,CAAC;WAEjD,UAAU;GACjB,MAAM,MAAM,oBAAoB,QAAQ,SAAS,UAAU;AAC3D,UAAO,KAAK,wCAAwC,SAAS,IAAI,IAAI,iBAAiB;AACtF,iBAAc,EAAE,UAAU,CAAC,QAAQ,EAAE;;MAGvC,eAAc,EAAE,UAAU,CAAC,QAAQ,EAAE;AAEvC,MAAI,aAAa,SAAS,GAAG;GAI3B,MAAM,kBAAkB,CAAC,GAAG,IAAI,IAAI,aAAa,CAAC;AAClD,eAAY,YAAY;AACxB,eAAY,WAAW,gBAAgB,KAAK,KAAK;;AAMnD,YAAU,WAAW,UAAUA,SAAO,YAAY;AAClD,KAAG,cAAc,SAAS,KAAK,UAAU,aAAa,MAAM,EAAE,EAAE;GAC9D,UAAU;GACV,MAAM;GACN,MAAM;GACP,CAAC;AACF,KAAG,WAAW,SAAS,SAAS;AAChC,YAAU;AAEV,MAAI,CAAC,aACH,UAAS,KAAK,QAAQ;AAExB,SAAO,KAAK,uBAAuB,WAAW;AAC9C,SAAO;GAAE,MAAM;GAAW;GAAU;UAC7B,KAAK;EACZ,MAAM,MAAM,eAAe,QAAQ,IAAI,UAAU;AACjD,SAAO,MAAM,mCAAmC,MAAM;AACtD,SAAO;GAAE,MAAM;GAAU,OAAO;GAAK;WAC7B;AACR,MAAI,QACF,KAAI;AACF,MAAG,WAAW,QAAQ;UAChB;;;;;;;;AAYd,eAAsB,eACpB,KACA,KACA,SACA,aACA,UACA,UACA,UAMA,SACA,SACuB;CACvB,MAAM,SAAS,SAAS;AACxB,KAAI,CAAC,OAAQ,QAAO;CAEpB,MAAM,YAAY,OAAO;CAGzB,MAAM,YAAY,gBAAgB,wBAAwB,WAAW;CACrE,MAAM,cAAc,UAAU;AAE9B,KAAI,CAAC,aAAa;AAChB,WAAS,OAAO,KAAK,4CAA4C,YAAY,kBAAkB;AAC/F,SAAO;;CAGT,IAAI;AACJ,KAAI;AACF,WAAS,mBAAmB,aAAa,SAAS;UAC3C,KAAK;EACZ,MAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAC5D,WAAS,OAAO,MACd,sCAAsC,YAAY,KAAK,YAAY,IAAI,IAAI,GAC5E;AACD,qBACE,KACA,KACA,KAAK,UAAU,EACb,OAAO;GAAE,SAAS,yBAAyB;GAAe,MAAM;GAAe,EAChF,CAAC,CACH;AACD,SAAO;;AAGT,UAAS,OAAO,KAAK,kCAAkC,cAAc,WAAW;CAGhF,MAAM,iBAAiB,qBACrB,KACA,QACA,aACA,OAAO,eAAe,WACvB;AACD,KAAI,CAAC,gBAAgB;AACnB,qBACE,KACA,KACA,KAAK,UAAU,EACb,OAAO;GAAE,SAAS;GAAqC,MAAM;GAAe,EAC7E,CAAC,CACH;AACD,SAAO;;CAGT,MAAM,cAAc,WAAW,KAAK,UAAU,QAAQ;CAGtD,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI,kBAAkB;CACtB,IAAI;CACJ,IAAI,aAAa;CACjB,IAAI,sBAAsB;CAI1B,IAAI,mBAAmB;CACvB,IAAI,qBAAqB;CACzB,IAAI,UAAU;CACd,IAAI,kBAA4B,EAAE;CAClC,IAAI,kBAAkB;CACtB,MAAM,sBAAsB,oBAAoB,OAAO,oBAAoB;CAC3E,MAAM,uBAAuB,qBAAqB,OAAO,qBAAqB;AAC9E,KAAI;EACF,MAAM,SAAS,MAAM,oBACnB,QACA,gBACA,aACA,KACA,IAAI,QACJ,SAAS,QACT;GAAE,mBAAmB,OAAO;GAAmB,eAAe,OAAO;GAAe,EACpF,qBACA,qBACD;AACD,mBAAiB,OAAO;AACxB,oBAAkB,OAAO;AACzB,iBAAe,OAAO;AACtB,cAAY,OAAO;AACnB,qBAAmB,OAAO;AAC1B,uBAAqB,OAAO;AAC5B,YAAU,OAAO;AACjB,oBAAkB,OAAO;AACzB,oBAAkB,OAAO;AACzB,oBAAkB,OAAO;AACzB,kBAAgB,OAAO;AACvB,eAAa,OAAO;AACpB,wBAAsB,OAAO;UACtB,KAAK;EACZ,MAAM,MAAM,eAAe,QAAQ,IAAI,UAAU;AACjD,WAAS,OAAO,MAAM,yBAAyB,MAAM;AACrD,MAAI,CAAC,IAAI,aAAa;AACpB,OAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,OAAI,IACF,KAAK,UAAU,EACb,OAAO;IAAE,SAAS,6BAA6B;IAAO,MAAM;IAAe,EAC5E,CAAC,CACH;QAOD,KAAI,SAAS;AAEf,SAAO;;AAkBT,KAAI,iBAAiB;EACnB,MAAM,YACJ,kBAAkB,UACd,OAAO,qBAAqB,cAC5B,OAAO,oBAAoB;AACjC,MAAI,CAAC,oBAAoB,uBAAuB,CAAC,IAAI,aAAa;GAChE,MAAM,aAAa,KAAK,MAAM,6BAA6B,OAAO,MAAM;AACxE,YAAS,OAAO,MACd,kCAAkC,WAAW,+BAA+B,WAAW,qHACxF;AACD,OAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,OAAI,IACF,KAAK,UAAU,EACb,OAAO;IACL,SAAS,6BAA6B,WAAW;IACjD,MAAM;IACP,EACF,CAAC,CACH;AACD,UAAO;;AAET,WAAS,OAAO,KACd,8BAA8B,UAAU,QAAQ,WAAW,gDAC5D;AACD,MAAI,CAAC,oBAAoB,CAAC,IAAI,aAAa;GAIzC,MAAM,eAAe,kBAAkB,OAAO,iBAAiB,MAAM,MAAM;GAC3E,MAAM,eAAuC,EAAE;GAC/C,MAAM,KAAK,gBAAgB;AAE3B,gBAAa,mBADC,MAAM,QAAQ,GAAG,GAAG,GAAG,KAAK,KAAK,GAAI,MAAM,OACjB;AACxC,OAAI,UAAU,cAAc,aAAa;AACzC,OAAI,IAAI,UAAU;aACT,CAAC,IAAI,cACd,KAAI,KAAK;AAEX,SAAO;;CAOT,MAAM,cAAc,gBAAgB;CACpC,MAAM,WAAW,gBAAgB,YAAY;CAC7C,MAAM,iBAAiB,SAAS,aAAa,CAAC,SAAS,qCAAqC;CAI5F,MAAM,eAAe,4CACnB,UACA,aACA,iBAAiB,YAAY,cAC7B,SAAS,OACV;CACD,MAAM,YACJ,gBAAgB,SAAS,mBAAmB,aACxC,2BAA2B,aAAa,GACxC;CAEN,IAAI;AAUJ,KAFwB,SAAS,aAAa,CAAC,WAAW,SAAS,IAC5C,kBAAkB,OAAO,iBAAiB,KAC1B;EAErC,MAAM,cAAc,SACjB,aAAa,CACb,QAAQ,UAAU,GAAG,CACrB,QAAQ,QAAQ,MAAM,CACtB,MAAM,IAAI,CAAC,GACX,MAAM;AACT,MAAI,UAAU,WAAW,EACvB,UAAS,OAAO,KACd,2EACD;AAEH,oBAAkB;GAChB,OAAO,UAAU,SAAS,SAAS;GACnC,GAAI,eAAe,gBAAgB,QAAQ,EAAE,QAAQ,aAAa,GAAG,EAAE;GACxE;YACQ,WAAW;AAEpB,WAAS,OAAO,KAAK,gCAAgC,SAAS,2BAA2B;AACzF,MAAI,UAAU,UACZ,UAAS,OAAO,KAAK,gEAAgE;AAEvF,MAAI,UAAU,iBAAiB,UAAU,gBAAgB,EACvD,UAAS,OAAO,KACd,GAAG,UAAU,cAAc,0CAA0C,UAAU,qBAAqB,aAAa,UAAU,uBAAuB,KACnJ;AAEH,MAAI,UAAU,gBACZ,UAAS,OAAO,KACd,sEAAsE,UAAU,cAAc,KAAK,UAAU,YAAY,KAAK,KAC/H;EAcH,MAAM,iBAAiB,sBAAsB,UAAU,MAAM;EAC7D,MAAM,cAAc,iBAAiB,EAAE,OAAO,gBAAgB,GAAG,EAAE;AAKnE,MAAI,UAAU,cACZ,mBAAkB,EAAE,eAAe,UAAU,eAAe;WACnD,UAAU,UAAU;GAC7B,MAAM,uBACJ,UAAU,aAAa,UAAU,UAAU,SAAS,IAChD,EACE,WAAW,UAAU,UAAU,KAAK,QAAQ;IAC1C,GAAG;IACH,MAAM,GAAG,QAAQ;IACjB,WAAW,GAAG,aAAa;IAC5B,EAAE,EACJ,GACD,EAAE;GACR,MAAM,qBAAqB,UAAU,UAAU,EAAE,SAAS,UAAU,SAAS,GAAG,EAAE;GAClF,MAAM,uBAAuB,UAAU,YAAY,EAAE,WAAW,UAAU,WAAW,GAAG,EAAE;AAC1F,qBAAkB;IAChB,OAAO;KACL,SAAS,UAAU;KACnB,aAAa,UAAU,iBAAiB;KACzC;IACD,GAAG;IACH,GAAG;IACH,GAAG;IACJ;aAED,UAAU,YAAY,OACrB,CAAC,UAAU,aAAa,UAAU,UAAU,WAAW,MACxD,CAAC,UAAU,iBAAiB,QAC5B;AACA,YAAS,OAAO,KAAK,qEAAqE;GAC1F,MAAM,kBAAkB,UAAU,YAAY,EAAE,WAAW,UAAU,WAAW,GAAG,EAAE;GAKrF,MAAM,2BACJ,UAAU,aAAa,UAAU,qBAC7B,EAAE,oBAAoB,UAAU,oBAAoB,GACpD,EAAE;AACR,gCACE,SAAS,QACT,UAAU,WACV,UAAU,mBACX;GAID,MAAM,yBAAyB,UAAU,kBAAkB,SACvD,EAAE,kBAAkB,UAAU,kBAAkB,GAChD,EAAE;GACN,MAAM,oBAAoB,UAAU,aAAa,SAC7C,EAAE,aAAa,UAAU,aAAa,GACtC,EAAE;AACN,qBAAkB;IAChB,SAAS,UAAU,WAAW;IAC9B,GAAG;IACH,GAAG;IACH,GAAG;IACH,GAAG;IACH,GAAG;IACJ;SACI;GACL,MAAM,kBAAkB,UAAU,YAAY,EAAE,WAAW,UAAU,WAAW,GAAG,EAAE;GAGrF,MAAM,2BACJ,UAAU,aAAa,UAAU,qBAC7B,EAAE,oBAAoB,UAAU,oBAAoB,GACpD,EAAE;AACR,gCACE,SAAS,QACT,UAAU,WACV,UAAU,mBACX;GAID,MAAM,yBAAyB,UAAU,kBAAkB,SACvD,EAAE,kBAAkB,UAAU,kBAAkB,GAChD,EAAE;GACN,MAAM,oBAAoB,UAAU,aAAa,SAC7C,EAAE,aAAa,UAAU,aAAa,GACtC,EAAE;AACN,OAAI,UAAU,iBAAiB,QAAQ;IAMrC,MAAM,aAAa,UAAU,aAAa,EAAE,EAAE,IAAI,yBAAyB;IAC3E,MAAM,kBAAkB,UAAU,gBAAgB,KAAK,QAAQ;KAC7D,GAAG;KACH,MAAM,GAAG,QAAQ;KACjB,OAAO,GAAG,SAAS;KACpB,EAAE;IACH,MAAM,cAAc,UAAU,iBAAiB,SAC3C,EAAE,iBAAiB,UAAU,iBAAiB,GAC9C,UAAU,QAAQ,SAChB,EAAE,QAAQ,UAAU,QAAQ,GAC5B,EAAE;AACR,sBAAkB;KAChB,GAAI,UAAU,UAAU,EAAE,SAAS,UAAU,SAAS,GAAG,EAAE;KAC3D;KACA;KACA,GAAG;KACH,GAAG;KACH,GAAG;KACH,GAAG;KACH,GAAG;KACH,GAAG;KACJ;cACQ,UAAU,aAAa,UAAU,UAAU,SAAS,GAAG;IAChE,MAAM,qBAAqB,UAAU,UAAU,IAAI,yBAAyB;AAC5E,QAAI,UAAU,SAAS;KAWrB,MAAM,eAAe,UAAU,QAAQ,SAAS,EAAE,QAAQ,UAAU,QAAQ,GAAG,EAAE;AACjF,uBAAkB;MAChB,SAAS,UAAU;MACnB,WAAW;MACX,GAAG;MACH,GAAG;MACH,GAAG;MACH,GAAG;MACH,GAAG;MACH,GAAG;MACJ;UAED,mBAAkB;KAChB,WAAW;KACX,GAAG;KACH,GAAG;KACH,GAAG;KACH,GAAG;KACH,GAAG;KACJ;SAGH,mBAAkB;IAChB,SAAS,UAAU,WAAW;IAC9B,GAAG;IACH,GAAG;IACH,GAAG;IACH,GAAG;IACH,GAAG;IACJ;;QAGA;EAEL,IAAI,iBAA0B;AAC9B,MAAI;AACF,oBAAiB,KAAK,MAAM,aAAa;WAClC,UAAU;GACjB,MAAM,MAAM,oBAAoB,QAAQ,SAAS,UAAU;AAC3D,YAAS,OAAO,KACd,wCAAwC,IAAI,6BAC7C;;AAMH,OACG,QAAQ,kBAAkB,SACzB,QAAQ,kBAAkB,6BAC1B,QAAQ,kBAAkB,sBAC1B,QAAQ,kBAAkB,0BAC1B,QAAQ,kBAAkB,8BAC5B,mBAAmB,MACnB;GACA,MAAM,MAAM;AAKZ,OAHE,OAAO,IAAI,UAAU,YACrB,IAAI,UAAU,QACd,OAAQ,IAAI,MAAkC,YAAY,UAC1C;IAChB,MAAM,MAAM,IAAI;AAChB,sBAAkB;KAChB,OAAO;MACL,SAAS,OAAO,IAAI,WAAW,gBAAgB;MAC/C,MAAM,OAAO,IAAI,QAAQ,YAAY;MACrC,MAAM,IAAI,OAAO,OAAO,IAAI,KAAK,GAAG;MACrC;KACD,QAAQ;KACT;SAED,mBAAkB;IAAE,MAAM;IAAgB,QAAQ;IAAgB;QAMpE,mBAAkB,qBAChB,gBACA,gBACA,SAAS,QACT,QACD;;AAYL,KAAI,oBAAoB;AACtB,MAAI,CAAC,SAAS;AACZ,YAAS,OAAO,KACd,iFACD;AACD,UAAO;;AAET,WAAS,OAAO,KACd,qGACD;;CAMH,IAAI;AACJ,KAAI,gBAAgB,SAAS,GAAG;EAC9B,MAAM,KAAK;AACX,oBAAkB;GAChB,QAAQ,GAAG,KAAK;GAChB,oBAAoB,GAAG,MAAM,EAAE,CAAC,KAAK,GAAG,MAAM,IAAI,GAAG,GAAG;GACxD,iBAAiB,GAAG,GAAG,SAAS,KAAK;GACtC;;CAGH,MAAM,eAAe,SAAS,mBAAmB,SAAS,iBAAiB,QAAQ,GAAG;CACtF,MAAM,WAAW,qBAAqB,QAAQ;CAC9C,MAAM,UAAmB;EACvB,OAAO,kBAAkB,cAAc,SAAS,OAAO;EACvD,UAAU;EACV,GAAI,mBAAmB,EAAE,iBAAiB;EAC1C,GAAI,YAAY,EAAE,UAAU;EAC7B;CAED,MAAM,kBAA4B,EAAE;AACpC,KAAI,WAAW,UACb,iBAAgB,KAAK,4DAA4D;CAEnF,MAAM,gBAAgB,eAAe;EACnC;EACA;EACA,QAAQ,UAAU,IAAI;EACtB;EACA;EACA,UAAU;EACV,QAAQ,SAAS;EAClB,CAAC;AACF,KAAI,cAAc,SAAS,UAAU;AACnC,MAAI,CAAC,IAAI,YACP,KAAI,UAAU,yBAAyB,oBAAoB,cAAc,MAAM,CAAC;MAEhF,UAAS,OAAO,KAAK,iEAAiE;AAExF,WAAS,OAAO,KAAK,2DAA2D;;AAMlF,KAAI,kBAIF;MAAI,SAAS,uBAAuB,QAAQ,gBAAgB;GAC1D,MAAM,eAAuE,iBACzE,oBACA,SAAS,aAAa,CAAC,SAAS,uBAAuB,GACrD,oBACA;AACN,OAAI;AACF,YAAQ,eAAe,aAAa;YAC7B,KAAK;AACZ,aAAS,OAAO,KACd,kCAAkC,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GACnF;;;QAGA;AAIL,MAAI,SAAS,qBAAqB;GAChC,IAAI;AACJ,OAAI;AACF,cAAU,MAAM,QAAQ,oBAAoB;KAC1C,QAAQ;KACR,aAAa;KACb,MAAM;KACP,CAAC;YACK,KAAK;IACZ,MAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAC5D,UAAM,IAAI,MAAM,uCAAuC,YAAY,IAAI,MAAM;;AAE/E,OAAI,QAAS,QAAO;;EAMtB,MAAM,eAAe,kBAAkB,OAAO,iBAAiB,MAAM,MAAM;EAC3E,MAAM,eAAe,SAAS,aAAa,CAAC,WAAW,SAAS;EAIhE,MAAM,eAAuC,EAAE;EAC/C,MAAM,WACH,gBAAgB,OAAO,eAAe,OAAQ,CAAC,eAC5C,YAAY,qBACZ;AACN,MAAI,SACF,cAAa,kBAAkB;AAEjC,MAAI,UAAU,cAAc,aAAa;AACzC,MAAI,IAAI,kBAAkB,eAAe,YAAY,aAAa;;AAGpE,QAAO;;;;;;;;;;AAeT,IAAa,wBAAb,MAAmC;CACjC,AAAQ,UAAU,IAAI,cAAc,OAAO;;CAE3C,MAAM,OAAuB;AAC3B,SAAO,KAAK,QAAQ,MAAM,MAAM;;;CAGlC,MAAc;AACZ,SAAO,KAAK,QAAQ,KAAK;;;;;;;;AAS7B,SAAgB,aAAa,OAA2B,UAA0B;AAChF,KAAI,SAAS,QAAQ,CAAC,OAAO,SAAS,MAAM,IAAI,SAAS,EAAG,QAAO;AACnE,QAAO;;;;;;;;AAST,SAAgB,oBAAoB,OAAmC;AACrE,KAAI,SAAS,QAAQ,CAAC,OAAO,SAAS,MAAM,IAAI,SAAS,EACvD,QAAO;AAET,QAAO,KAAK,IAAI,OAAO,0BAA0B;;;;;;;AAQnD,SAAgB,qBAAqB,OAAmC;AAOtE,KAAI,SAAS,QAAQ,CAAC,OAAO,SAAS,MAAM,IAAI,SAAS,EACvD,QAAO;AAET,QAAO,KAAK,MAAM,MAAM;;AAG1B,SAAS,oBACP,QACA,SACA,MACA,WACA,SAAiB,QACjB,QACA,UACA,iBAAyB,gCACzB,kBAA0B,iCAyCzB;AACD,QAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,YAAY,OAAO,aAAa,WAAW,QAAQC;EACzD,MAAM,sBAAsB,aAAa,UAAU,mBAAmB,IAAO;EAC7E,MAAM,kBAAkB,aAAa,UAAU,eAAe,IAAO;EAMrE,IAAI,kBAAkB;EACtB,MAAM,MAAM,UAAU,QACpB,QACA;GACE;GACA,SAAS;GACT,SAAS;IACP,GAAG;IACH,kBAAkB,OAAO,WAAW,KAAK,CAAC,UAAU;IACrD;GACF,GACA,QAAQ;AACP,OAAI,WAAW,uBAAuB;AACpC,QAAI,wBAAQ,IAAI,MAAM,qCAAqC,kBAAkB,IAAK,GAAG,CAAC;KACtF;GAOF,MAAM,KAAK,IAAI,QAAQ;GACvB,MAAM,QAAQ,gBAAgB,GAAG;GACjC,MAAM,UAAU,MAAM,aAAa;GACnC,MAAM,QAAQ,QAAQ,SAAS,oBAAoB;GACnD,MAAM,WAAW,QAAQ,SAAS,uBAAuB;GACzD,MAAM,sBAAsB,QAAQ,SAAS,qCAAqC;GAClF,MAAM,sBAAsB,SAAS,YAAY;GAGjD,MAAM,kBAA4B,EAAE;GAGpC,MAAM,kBAAkB,mBAAmB,KAAK,KAAK;GACrD,IAAI,cAAc;GAIlB,MAAM,eAAe,IAAI,uBAAuB;GAChD,IAAI,oBAAoB,OAAO,MAAM,EAAE;GAEvC,IAAI,mBAAmB;GACvB,IAAI,qBAAqB;GAGzB,IAAI,UAAU;AACd,OAAI,uBAAuB,aAAa,CAAC,UAAU,aAAa;IAC9D,MAAM,eAAuC;KAC3C,iBAAiB;KACjB,YAAY;KACZ,qBAAqB;KACtB;AACD,QAAI,MAAO,cAAa,kBAAkB;IAI1C,MAAM,YAAY,IAAI,cAAc;IACpC,MAAM,eAAe,aAAa,OAAO,YAAY,MAAM,MAAM;AACjE,cAAU,UAAU,cAAc,aAAa;AAG/C,QAAI,OAAO,UAAU,iBAAiB,WAAY,WAAU,cAAc;AAC1E,uBAAmB;AACnB,cAAU,GAAG,eAAe;AAC1B,SAAI,CAAC,UAAU,kBAAkB;AAC/B,2BAAqB;AACrB,UAAI,CAAC,SAAS;AAIZ,WAAI,SAAS;AACb,WAAI,eAAe,QAAQ,eAAe;AAC1C,cAAO,SAAS;AAChB,uBAAgB;AAChB,uBAAgB,SAAS;AACzB,qBAAc;AACd,2BAAoB,OAAO,MAAM,EAAE;;;MASvC;;GAEJ,MAAM,SAAmB,EAAE;GAM3B,IAAI,gBAAgB;GACpB,IAAI,aAAa;GACjB,IAAI,kBAAkB;;GAEtB,IAAI;GAKJ,MAAM,cAAc,sBAAsB;;;;;;;GAO1C,IAAI,sBAAsB;GAkB1B,MAAM,kBAAkB,QAA0B;AAChD,sBAAkB;AAClB,oBAAgB;AAChB,oBAAgB,SAAS;AACzB,kBAAc;AAKd,iBAAa,KAAK;AAClB,wBAAoB,OAAO,MAAM,EAAE;AACnC,QAAI,qBAAqB;AACvB,YAAO,SAAS;AAChB,qBAAgB;;IAKlB,MAAM,SACJ,QAAQ,SACJ,aAAa,eAAe,WAC5B,cAAc,gBAAgB;AACpC,YAAQ,KACN,+CAA+C,OAAO,4FACvD;;GAEH,MAAM,kBAAkB,UAAkB;AACxC,kBAAc,MAAM;AAQpB,QAAI,CAAC,mBAAmB,gBAAgB,MAAM,SAAS,eACrD,gBAAe,OAAO;aACb,CAAC,mBAAmB,gBAAgB,UAAU,gBACvD,gBAAe,QAAQ;AASzB,QAAI,CAAC,iBAAiB;AACpB,YAAO,KAAK,MAAM;AAClB,sBAAiB,MAAM;eACd,CAAC,oBACV,KAAI,gBAAgB,MAAM,UAAU,aAAa;AAC/C,YAAO,KAAK,MAAM;AAClB,sBAAiB,MAAM;UAMvB,uBAAsB;AAS1B,QAAI,CAAC,oBAAoB,SAAS,WAAW;AAC3C,oBAAe,aAAa,MAAM,MAAM;KAOxC,MAAM,YAAY,WAAW,UAAU;KACvC,MAAM,QAAQ,YAAY,MAAM,UAAU;AAM1C,UAAK,IAAI,KAAK,GAAG,KAAK,MAAM,SAAS,GAAG,MAAM;AAC5C,UAAI,gBAAgB,UAAU,iBAAiB;AAC7C,sBAAe,QAAQ;AACvB;;MAEF,MAAM,QAAQ,MAAM,IAAI,MAAM;AAC9B,UAAI,MAAM,SAAS,GAAG;AACpB,uBAAgB,KAAK,KAAK,KAAK,CAAC;AAGhC,WAAI,mBAAmB,MAAM,CAAE,WAAU;;;AAK7C,SAAI,CAAC,gBACH,eAAc,MAAM,MAAM,SAAS;;AASvC,QAAI,CAAC,mBAAmB,oBAatB,KAAI,gBAAgB,kBAAkB,SAAS,eAC7C,gBAAe,OAAO;SACjB;AACL,yBAAoB,OAAO,OAAO,CAAC,mBAAmB,MAAM,CAAC;AAC7D,YAAO,kBAAkB,UAAU,GAAG;AACpC,UAAI,gBAAgB,UAAU,iBAAiB;AAG7C,sBAAe,QAAQ;AACvB;;MAEF,MAAM,WAAW,kBAAkB,aAAa,EAAE;AAClD,UAAI,WAAW,MAAM,kBAAkB,SAAS,SAAU;AAC1D,sBAAgB,KAAK,KAAK,KAAK,CAAC;AAChC,0BAAoB,kBAAkB,SAAS,SAAS;;;AAK9D,QACE,oBACA,aACA,CAAC,sBACD,CAAC,UAAU,aACX,CAAC,UAAU,cAEX,KAAI;AACF,eAAU,MAAM,MAAM;aACf,UAAU;AACjB,aAAQ,MACN,oCAAoC,oBAAoB,QAAQ,SAAS,UAAU,YACpF;AACD,0BAAqB;;;AAI3B,OAAI,GAAG,QAAQ,eAAe;AAC9B,OAAI,GAAG,SAAS,OAAO;AACvB,OAAI,GAAG,aAAa;AAClB,QAAI,IAAI,OAAQ,KAAI,WAAW,EAAE;AAOjC,QAAI,CAAC,oBAAoB,SAAS,WAAW;AAG3C,oBAAe,aAAa,KAAK;AACjC,SAAI,YAAY,MAAM,CAAC,SAAS,EAC9B,iBAAgB,KAAK,KAAK,KAAK,CAAC;;IAGpC,MAAM,YAAY,OAAO,OAAO,OAAO;AACvC,QACE,oBACA,aACA,CAAC,sBACD,CAAC,UAAU,aACX,CAAC,UAAU,cAEX,KAAI;AACF,eAAU,KAAK;aACR,QAAQ;AACf,aAAQ,MACN,kCAAkC,kBAAkB,QAAQ,OAAO,UAAU,YAC9E;;IAWL,MAAM,aACJ,CAAC,mBAAoB,mBAAmB,CAAC,mBAAoB,UAAU,UAAU,GAAG;AACtF,YAAQ;KACN,QAAQ,IAAI,cAAc;KAC1B,SAAS,IAAI;KACb,MAAM;KACN;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACD,CAAC;KACF;IAEL;AACD,MAAI,GAAG,iBAAiB;AACtB,OAAI,wBACF,IAAI,MACF,oCAAoC,sBAAsB,IAAK,KAAK,OAAO,OAC5E,CACF;IACD;AACF,MAAI,GAAG,SAAS,OAAO;AAGvB,oBAAkB,KAAK,KAAK;AAC5B,MAAI,MAAM,KAAK;AACf,MAAI,KAAK;GACT;;;;;;;;AASJ,SAAS,6BACP,QACA,WACA,oBACM;AACN,KAAI,sBAAsB,CAAC,UACzB,SAAQ,KAAK,gFAAgF;;;AAKjG,SAAS,yBAAyB,IAAwB;AACxD,QAAO;EAAE,GAAG;EAAI,MAAM,GAAG,QAAQ;EAAI,WAAW,GAAG,aAAa;EAAM;;;;;;;;;;AAWxE,SAAS,oBAAoB,KAAsB;AACjD,KAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,KAAI,QAAQ,UAAa,QAAQ,KAAM,QAAO;AAC9C,QAAO,KAAK,UAAU,IAAI;;;;;;;;;;;;AAa5B,MAAM,sBAA2C,IAAI,IAAI;CACvD;CACA;CACA;CACD,CAAC;;;;;;;;;;;;;;;;;;;;;;;AAwBF,SAAgB,sBAAsB,KAAsD;AAC1F,KAAI,OAAO,QAAQ,YAAY,QAAQ,QAAQ,MAAM,QAAQ,IAAI,CAAE,QAAO;CAC1E,MAAM,MAA+B,EAAE;AACvC,MAAK,MAAM,CAAC,KAAK,QAAQ,OAAO,QAAQ,IAA+B,EAAE;AACvE,MAAI,QAAQ,UAAa,QAAQ,KAAM;AACvC,MAAI,oBAAoB,IAAI,IAAI,EAAE;AAChC,OAAI,OAAO,QAAQ,YAAY,QAAQ,QAAQ,MAAM,QAAQ,IAAI,CAAE;GACnE,MAAM,QAAiC,EAAE;AACzC,QAAK,MAAM,CAAC,UAAU,aAAa,OAAO,QAAQ,IAA+B,CAI/E,KAAI,OAAO,aAAa,YAAY,OAAO,SAAS,SAAS,CAAE,OAAM,YAAY;AAEnF,OAAI,OAAO,KAAK,MAAM,CAAC,SAAS,EAAG,KAAI,OAAO;AAC9C;;AAEF,MAAI,QAAQ,WAAW;AACrB,OAAI,OAAO,QAAQ,UAAW,KAAI,OAAO;AACzC;;AAEF,MAAI,OAAO,QAAQ,YAAY,OAAO,SAAS,IAAI,CAAE,KAAI,OAAO;;AAElE,QAAO,OAAO,KAAK,IAAI,CAAC,SAAS,IAAK,MAAqC;;;;;;AAO7E,SAAS,qBACP,QACA,QACA,QACA,SACiB;AACjB,KAAI,WAAW,QAAQ,WAAW,OAEhC,QAAO;EACL,OAAO;GAAE,SAAS;GAAuC,MAAM;GAAe;EAC9E;EACD;CAGH,MAAM,MAAM;AAIZ,KACE,OAAO,IAAI,UAAU,YACrB,IAAI,UAAU,QACd,OAAQ,IAAI,MAAkC,YAAY,UAC1D;EACA,MAAM,MAAM,IAAI;AAChB,SAAO;GACL,OAAO;IACL,SAAS,OAAO,IAAI,WAAW,gBAAgB;IAC/C,MAAM,OAAO,IAAI,QAAQ,YAAY;IACrC,MAAM,IAAI,OAAO,OAAO,IAAI,KAAK,GAAG;IACrC;GACD;GACD;;AAIH,KAAI,MAAM,QAAQ,IAAI,KAAK,IAAI,IAAI,KAAK,SAAS,GAAG;EAClD,MAAM,QAAQ,IAAI,KAAK;AACvB,MAAI,MAAM,QAAQ,MAAM,UAAU,CAChC,QAAO,EAAE,WAAW,MAAM,WAAuB;AAMnD,MAAI,OAAO,MAAM,cAAc,UAAU;GACvC,MAAM,MAAM,OAAO,KAAK,MAAM,WAAW,SAAS;AAClD,OAAI,IAAI,eAAe,KAAK,IAAI,aAAa,MAAM,EAIjD,SAAQ,KACN,iDAAiD,IAAI,WAAW,6DACjE;QACI;IAGL,MAAM,SAAS,IAAI,WAAW,IAAI;IAClC,MAAM,SAAS,IAAI,aAAa,OAAO,QAAQ,GAAG,IAAI,aAAa,EAAE;AACrE,WAAO,EAAE,WAAW,MAAM,KAAK,OAAO,EAAE;;;AAQ5C,MAAK,IAAI,KAAwC,MAAM,SAAS,KAAK,OAAO,KAAK,SAAS,EAAE;GAK1F,MAAM,YAAY,IAAI;GACtB,MAAM,SAAS,UACZ,QAAQ,SAAS,KAAK,OAAO,KAAK,SAAS,CAC3C,KAAK,UAAU;IACd,GAAI,KAAK,MAAM,EAAE,KAAK,OAAO,KAAK,IAAI,EAAE,GAAG,EAAE;IAC7C,GAAI,KAAK,WAAW,EAAE,SAAS,OAAO,KAAK,SAAS,EAAE,GAAG,EAAE;IAC3D,GAAI,KAAK,iBAAiB,EAAE,eAAe,OAAO,KAAK,eAAe,EAAE,GAAG,EAAE;IAC9E,EAAE;GAGL,MAAM,eAAe,UAAU,SAAS,OAAO;AAC/C,OAAI,eAAe,EACjB,SAAQ,KACN,WAAW,aAAa,+DACzB;AAEH,OAAI,OAAO,WAAW,EACpB,QAAO,EAAE,OAAO,OAAO,IAAI;AAE7B,UAAO,EAAE,QAAQ;;;AAKrB,KAAI,MAAM,QAAQ,IAAI,YAAY,EAAE;EAClC,MAAM,SAAU,IAAI,YAA+C,KAAK,OAAO;GAC7E,GAAI,EAAE,qBAAqB,EAAE,SAAS,OAAO,EAAE,mBAAmB,EAAE,GAAG,EAAE;GACzE,GAAI,EAAE,WAAW,EAAE,UAAU,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE;GACvD,EAAE;AACH,MAAI,OAAO,WAAW,EACpB,QAAO,EAAE,OAAO,OAAO,IAAI;AAE7B,SAAO,EAAE,QAAQ;;CAOnB,MAAM,yBACJ,SAAS,kBAAkB,mBAAmB,SAAS,kBAAkB;AAW3E,KATE,OAAO,IAAI,SAAS,aACnB,0BACC,IAAI,SAAS,gBACZ,IAAI,aAAa,UAAa,IAAI,aAAa,WAClD,EAAE,aAAa,QACf,EAAE,gBAAgB,QAClB,EAAE,YAAY,QACd,EAAE,aAAa,QACf,EAAE,aAAa,KAEf,QAAO,EACL,eAAe;EACb,MAAM,IAAI;EACV,GAAI,IAAI,WAAW,EAAE,UAAU,OAAO,IAAI,SAAS,EAAE,GAAG,EAAE;EAC1D,GAAI,MAAM,QAAQ,IAAI,UAAU,GAC5B,EACE,WAAW,IAAI,UACZ,QACE,aACC,OAAO,aAAa,YACpB,aAAa,QACb,OAAO,SAAS,SAAS,SAC5B,CACA,KAAK,cAAc,EAAE,MAAM,SAAS,MAAgB,EAAE,EAC1D,GACD,EAAE;EACN,GAAI,IAAI,aAAa,SAAY,EAAE,UAAU,OAAO,IAAI,SAAS,EAAE,GAAG,EAAE;EACxE,GAAI,IAAI,SAAS,OAAO,IAAI,UAAU,WAClC,EAAE,OAAO,IAAI,OAAkC,GAC/C,EAAE;EACN,GAAI,MAAM,QAAQ,IAAI,MAAM,GAAG,EAAE,OAAO,IAAI,OAAO,GAAG,EAAE;EACxD,GAAI,MAAM,QAAQ,IAAI,SAAS,GAAG,EAAE,UAAU,IAAI,UAAU,GAAG,EAAE;EAClE,EACF;AAIH,KACE,MAAM,QAAQ,IAAI,QAAQ,IAC1B,IAAI,QAAQ,SAAS,KACrB,EAAE,aAAa,QACf,EAAE,aAAa,QACf,EAAE,gBAAgB,MAClB;EACA,MAAM,UAAU,IAAI;EACpB,MAAM,gBAAgB,QAAQ,QAAQ,MAAM,EAAE,SAAS,gBAAgB;EACvE,MAAM,cAAc,QAAQ,QAAQ,MAAM,EAAE,SAAS,UAAU,OAAO,EAAE,SAAS,SAAS;EAC1F,MAAM,eAAe,cAAc,SAAS;EAC5C,MAAM,aAAa,YAAY,KAAK,MAAM,OAAO,EAAE,QAAQ,GAAG,CAAC,CAAC,KAAK,GAAG;EACxE,MAAM,aAAa,WAAW,SAAS;AAEvC,MAAI,cAAc;GAChB,MAAM,YAAwB,cAAc,KAAK,OAAO;IACtD,MAAM,OAAO,EAAE,KAAK;IACpB,WAAW,oBAAoB,EAAE,UAAU;IAC3C,GAAI,EAAE,KAAK,EAAE,IAAI,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE;IACrC,EAAE;AACH,OAAI,WACF,QAAO;IAAE,SAAS;IAAY;IAAW;AAE3C,UAAO,EAAE,WAAW;;AAEtB,MAAI,WACF,QAAO,EAAE,SAAS,YAAY;AAGhC,SAAO,EAAE,SAAS,IAAI;;AAOxB,KACE,OAAO,IAAI,OAAO,YAClB,OAAO,IAAI,WAAW,aACrB,IAAI,WAAW,eAAe,IAAI,WAAW,iBAAiB,IAAI,WAAW,aAC9E,EAAE,aAAa,QACf,EAAE,aAAa,QACf,EAAE,gBAAgB,QAClB,EAAE,aAAa,QACf,EAAE,UAAU,QACZ,EAAE,YAAY,QACd,EAAE,aAAa,QACf,EAAE,WAAW,QACb,EAAE,cAAc,QAChB,EAAE,UAAU,QACZ,EAAE,WAAW,QACb,EAAE,WAAW,MACb;AACA,MAAI,IAAI,WAAW,eAAe,IAAI,IACpC,QAAO,EACL,OAAO;GACL,IAAI,OAAO,IAAI,GAAG;GAClB,QAAQ;GACR,KAAK,OAAO,IAAI,IAAI;GACrB,EACF;AAEH,SAAO,EACL,OAAO;GACL,IAAI,OAAO,IAAI,GAAG;GAClB,QAAQ,IAAI,WAAW,WAAY,WAAsB;GAC1D,EACF;;AAIH,KAAI,MAAM,QAAQ,IAAI,UAAU,CAC9B,QAAO,EAAE,WAAW,IAAI,WAAuB;AAIjD,KAAI,MAAM,QAAQ,IAAI,QAAQ,IAAI,IAAI,QAAQ,SAAS,GAAG;EAExD,MAAM,UADS,IAAI,QAAQ,GACJ;AACvB,MAAI,SAAS;GACX,MAAM,eAAe,MAAM,QAAQ,QAAQ,WAAW,IAAI,QAAQ,WAAW,SAAS;GACtF,MAAM,aAAa,OAAO,QAAQ,YAAY,YAAY,QAAQ,QAAQ,SAAS;GAMnF,MAAM,kBACJ,OAAO,QAAQ,sBAAsB,YAAY,QAAQ,kBAAkB,SAAS,IAChF,QAAQ,oBACR,OAAO,QAAQ,cAAc,YAAY,QAAQ,UAAU,SAAS,IAClE,QAAQ,YACR;GAQR,MAAM,cAAc,sBAAsB,IAAI,MAAM;GACpD,MAAM,cAAc,cAAc,EAAE,OAAO,aAAa,GAAG,EAAE;AAE7D,OAAI,cAAc;IAChB,MAAM,YAAyB,QAAQ,WAA8C,KAClF,OAAO;KACN,MAAM,KAAK,GAAG;AACd,YAAO;MACL,MAAM,OAAO,GAAG,KAAK;MACrB,WAAW,oBAAoB,GAAG,UAAU;MAC5C,GAAI,GAAG,KAAK,EAAE,IAAI,OAAO,GAAG,GAAG,EAAE,GAAG,EAAE;MACvC;MAEJ;AACD,QAAI,WACF,QAAO;KACL,SAAS,QAAQ;KACjB;KACA,GAAI,kBAAkB,EAAE,WAAW,iBAAiB,GAAG,EAAE;KACzD,GAAG;KACJ;AAEH,WAAO;KACL;KACA,GAAI,kBAAkB,EAAE,WAAW,iBAAiB,GAAG,EAAE;KACzD,GAAG;KACJ;;AAGH,OAAI,WACF,QAAO;IACL,SAAS,QAAQ;IACjB,GAAI,kBAAkB,EAAE,WAAW,iBAAiB,GAAG,EAAE;IACzD,GAAG;IACJ;AAGH,UAAO;IACL,SAAS;IACT,GAAI,kBAAkB,EAAE,WAAW,iBAAiB,GAAG,EAAE;IACzD,GAAG;IACJ;;;AAKL,KAAI,MAAM,QAAQ,IAAI,QAAQ,IAAI,IAAI,QAAQ,SAAS,GAAG;EACxD,MAAM,SAAS,IAAI;EACnB,MAAM,gBAAgB,OAAO,QAAQ,MAAM,EAAE,SAAS,WAAW;EACjE,MAAM,aAAa,OAAO,QAAQ,MAAM,EAAE,SAAS,UAAU,OAAO,EAAE,SAAS,SAAS;EACxF,MAAM,iBAAiB,OAAO,QAAQ,MAAM,EAAE,SAAS,WAAW;EAKlE,MAAM,iBAAiB,OAAO,QAAQ,MAAM,EAAE,SAAS,oBAAoB;EAO3E,MAAM,mBAAmB,OACtB,KAAK,MAAM,qBAAqB,EAAE,CAAC,CACnC,QAAQ,SAAyB,SAAS,OAAU;EACvD,MAAM,eAAe,cAAc,SAAS;EAC5C,MAAM,aAAa,WAAW,KAAK,MAAM,OAAO,EAAE,QAAQ,GAAG,CAAC,CAAC,KAAK,GAAG;EACvE,MAAM,aAAa,WAAW,SAAS;EACvC,MAAM,qBACJ,eAAe,SAAS,IACpB,eAAe,KAAK,MAAM,OAAO,EAAE,YAAY,GAAG,CAAC,CAAC,KAAK,GAAG,GAC5D;EAQN,MAAM,qCAAqC;GACzC,IAAI;AACJ,QAAK,MAAM,KAAK,eACd,KAAI,OAAO,EAAE,cAAc,SAAU,OAAM,OAAO,EAAE,UAAU;AAEhE,UAAO;MACL;EAKJ,MAAM,2BACJ,sBAAsB,8BAClB,EAAE,oBAAoB,6BAA6B,GACnD,EAAE;AACR,+BAA6B,QAAQ,oBAAoB,4BAA4B;EACrF,MAAM,yBAAyB,iBAAiB,SAAS,IAAI,EAAE,kBAAkB,GAAG,EAAE;AAEtF,MAAI,cAAc;GAChB,MAAM,YAAwB,cAAc,KAAK,OAAO;IACtD,MAAM,OAAO,EAAE,KAAK;IACpB,WAAW,oBAAoB,EAAE,MAAM;IACvC,GAAI,EAAE,KAAK,EAAE,IAAI,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE;IACrC,EAAE;AACH,OAAI,WACF,QAAO;IACL,SAAS;IACT;IACA,GAAI,qBAAqB,EAAE,WAAW,oBAAoB,GAAG,EAAE;IAC/D,GAAG;IACH,GAAG;IACJ;AAEH,UAAO;IACL;IACA,GAAI,qBAAqB,EAAE,WAAW,oBAAoB,GAAG,EAAE;IAC/D,GAAG;IACH,GAAG;IACJ;;AAEH,MAAI,WACF,QAAO;GACL,SAAS;GACT,GAAI,qBAAqB,EAAE,WAAW,oBAAoB,GAAG,EAAE;GAC/D,GAAG;GACH,GAAG;GACJ;AAcH,MAAI,eAAe,SAAS,KAAK,eAAe,SAAS,EACvD,QAAO;GACL,SAAS;GACT,GAAI,qBAAqB,EAAE,WAAW,oBAAoB,GAAG,EAAE;GAC/D,GAAG;GACH,GAAG;GACJ;;AAKL,KAAI,MAAM,QAAQ,IAAI,WAAW,IAAI,IAAI,WAAW,SAAS,GAAG;EAE9D,MAAM,UADY,IAAI,WAAW,GACP;AAC1B,MAAI,WAAW,MAAM,QAAQ,QAAQ,MAAM,EAAE;GAC3C,MAAM,QAAQ,QAAQ;GAStB,MAAM,aAAa,MAAM,QAAQ,MAA+B;IAC9D,MAAM,SAAS,EAAE;AACjB,QACE,WAAW,UACX,WAAW,QACX,OAAO,OAAO,SAAS,YACvB,OAAO,KAAK,WAAW,EAEvB,QAAO;IAET,MAAM,KAAK,OAAO;AAClB,QAAI,OAAO,OAAO,YAAY,GAAG,SAAS,EACxC,QAAO,GAAG,WAAW,SAAS;AAGhC,WAAO;KACP;AACF,OAAI,WAAW,SAAS,GAAG;IACzB,MAAM,aAAa,WAAW,GAAG;AACjC,WAAO,EACL,OAAO;KACL,SAAS,OAAO,WAAW,QAAQ,GAAG;KACtC,aACE,OAAO,WAAW,aAAa,YAAY,WAAW,SAAS,SAAS,IACpE,WAAW,WACX;KACP,EACF;;GAGH,MAAM,cAAc,MAAM,QAAQ,MAAM,EAAE,aAAa;GACvD,MAAM,YAAY,MAAM,QAAQ,MAAM,OAAO,EAAE,SAAS,YAAY,CAAC,EAAE,QAAQ;GAC/E,MAAM,eAAe,MAAM,QAAQ,MAAM,EAAE,YAAY,QAAQ,OAAO,EAAE,SAAS,SAAS;GAC1F,MAAM,eAAe,YAAY,SAAS;GAC1C,MAAM,aAAa,UAAU,KAAK,MAAM,OAAO,EAAE,QAAQ,GAAG,CAAC,CAAC,KAAK,GAAG;GACtE,MAAM,aAAa,WAAW,SAAS;GACvC,MAAM,kBACJ,aAAa,SAAS,IAClB,aAAa,KAAK,MAAM,OAAO,EAAE,QAAQ,GAAG,CAAC,CAAC,KAAK,GAAG,GACtD;AAEN,OAAI,cAAc;IAChB,MAAM,YAAwB,YAAY,KAAK,MAAM;KACnD,MAAM,KAAK,EAAE;AACb,YAAO;MACL,MAAM,OAAO,GAAG,KAAK;MACrB,WAAW,oBAAoB,GAAG,KAAK;MACxC;MACD;AACF,QAAI,WACF,QAAO;KACL,SAAS;KACT;KACA,GAAI,kBAAkB,EAAE,WAAW,iBAAiB,GAAG,EAAE;KAC1D;AAEH,WAAO;KAAE;KAAW,GAAI,kBAAkB,EAAE,WAAW,iBAAiB,GAAG,EAAE;KAAG;;AAElF,OAAI,WACF,QAAO;IACL,SAAS;IACT,GAAI,kBAAkB,EAAE,WAAW,iBAAiB,GAAG,EAAE;IAC1D;AAGH,UAAO;IAAE,SAAS;IAAI,GAAI,kBAAkB,EAAE,WAAW,iBAAiB,GAAG,EAAE;IAAG;;;AAKtF,KAAI,IAAI,UAAU,OAAO,IAAI,WAAW,UAAU;EAEhD,MAAM,MADS,IAAI,OACA;AACnB,MAAI,OAAO,MAAM,QAAQ,IAAI,QAAQ,EAAE;GACrC,MAAM,SAAS,IAAI;GACnB,MAAM,gBAAgB,OAAO,QAAQ,MAAM,EAAE,QAAQ;GACrD,MAAM,aAAa,OAAO,QAAQ,MAAM,OAAO,EAAE,SAAS,SAAS;GACnE,MAAM,kBAAkB,OAAO,QAAQ,MAAM,EAAE,iBAAiB;GAChE,MAAM,eAAe,cAAc,SAAS;GAC5C,MAAM,aAAa,WAAW,KAAK,MAAM,OAAO,EAAE,QAAQ,GAAG,CAAC,CAAC,KAAK,GAAG;GACvE,MAAM,aAAa,WAAW,SAAS;GACvC,MAAM,mBACJ,gBAAgB,SAAS,IACrB,gBACG,KAAK,MAAM;IAEV,MAAM,KADK,EAAE,kBACE;AACf,WAAO,OAAO,IAAI,QAAQ,GAAG;KAC7B,CACD,KAAK,GAAG,GACX;AAEN,OAAI,cAAc;IAChB,MAAM,YAAwB,cAAc,KAAK,MAAM;KACrD,MAAM,KAAK,EAAE;AACb,YAAO;MACL,MAAM,OAAO,GAAG,QAAQ,GAAG;MAC3B,WAAW,oBAAoB,GAAG,MAAM;MACxC,GAAI,GAAG,YAAY,EAAE,IAAI,OAAO,GAAG,UAAU,EAAE,GAAG,EAAE;MACrD;MACD;AACF,QAAI,WACF,QAAO;KACL,SAAS;KACT;KACA,GAAI,mBAAmB,EAAE,WAAW,kBAAkB,GAAG,EAAE;KAC5D;AAEH,WAAO;KAAE;KAAW,GAAI,mBAAmB,EAAE,WAAW,kBAAkB,GAAG,EAAE;KAAG;;AAEpF,OAAI,WACF,QAAO;IACL,SAAS;IACT,GAAI,mBAAmB,EAAE,WAAW,kBAAkB,GAAG,EAAE;IAC5D;AAGH,UAAO;IAAE,SAAS;IAAI,GAAI,mBAAmB,EAAE,WAAW,kBAAkB,GAAG,EAAE;IAAG;;;AAOxF,KACE,OAAO,IAAI,kBAAkB,YAC7B,IAAI,WACJ,OAAO,IAAI,YAAY,YACvB,MAAM,QAAS,IAAI,QAAoC,QAAQ,EAC/D;EACA,MAAM,MAAM,IAAI;EAChB,MAAM,gBAAgB,IAAI;EAG1B,MAAM,aAAa,cAChB,QAAQ,MAAM,EAAE,SAAS,UAAU,OAAO,EAAE,SAAS,SAAS,CAC9D,KAAK,MAAM,OAAO,EAAE,QAAQ,GAAG,CAAC,CAChC,KAAK,GAAG;EACX,MAAM,aAAa,WAAW,SAAS;EACvC,MAAM,iBAAiB,cAAc,QAAQ,MAAM,EAAE,SAAS,YAAY;EAG1E,MAAM,eAAe,MAAM,QAAQ,IAAI,WAAW,GAC7C,IAAI,aACL,EAAE;AAEN,MAAI,eAAe,SAAS,GAAG;GAC7B,MAAM,YAAwB,eAAe,KAAK,OAAO;IACvD,MAAM,OAAO,EAAE,QAAS,EAAE,UAAsC,QAAQ,GAAG;IAC3E,WACE,OAAO,EAAE,eAAe,WACpB,EAAE,aACF,OAAO,EAAE,eAAe,WACtB,KAAK,UAAU,EAAE,WAAW,GAC5B,OAAQ,EAAE,UAAsC,cAAc,WAC5D,OAAQ,EAAE,SAAqC,UAAU,GACzD,oBAAqB,EAAE,UAAsC,UAAU;IACjF,GAAI,EAAE,KAAK,EAAE,IAAI,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE;IACrC,EAAE;AACH,OAAI,WACF,QAAO;IAAE,SAAS;IAAY;IAAW;AAE3C,UAAO,EAAE,WAAW;;AAEtB,MAAI,aAAa,SAAS,GAAG;GAC3B,MAAM,YAAwB,aAAa,KAAK,OAAO;IACrD,MAAM,KAAK,GAAG;AACd,WAAO;KACL,MAAM,OAAO,GAAG,QAAQ,IAAI,QAAQ,GAAG;KACvC,WACE,OAAO,GAAG,eAAe,WACrB,GAAG,aACH,OAAO,GAAG,eAAe,WACvB,KAAK,UAAU,GAAG,WAAW,GAC7B,OAAO,IAAI,cAAc,WACvB,OAAO,GAAG,UAAU,GACpB,oBAAoB,IAAI,UAAU;KAC5C,GAAI,GAAG,KAAK,EAAE,IAAI,OAAO,GAAG,GAAG,EAAE,GAAG,EAAE;KACvC;KACD;AACF,OAAI,WACF,QAAO;IAAE,SAAS;IAAY;IAAW;AAE3C,UAAO,EAAE,WAAW;;AAEtB,MAAI,WACF,QAAO,EAAE,SAAS,YAAY;AAMhC,SAAO,EAAE,SAAS,IAAI;;AAIxB,KAAI,IAAI,WAAW,OAAO,IAAI,YAAY,UAAU;EAClD,MAAM,MAAM,IAAI;EAChB,MAAM,qBAAqB,MAAM,QAAQ,IAAI,WAAW,IAAI,IAAI,WAAW,SAAS;EACpF,MAAM,mBAAmB,OAAO,IAAI,YAAY,YAAY,IAAI,QAAQ,SAAS;AAEjF,MAAI,oBAAoB;GACtB,MAAM,YAAyB,IAAI,WAChC,QAAQ,OAAO,GAAG,YAAY,KAAK,CACnC,KAAK,OAAO;IACX,MAAM,KAAK,GAAG;AACd,WAAO;KACL,MAAM,OAAO,GAAG,QAAQ,GAAG;KAC3B,WAAW,oBAAoB,GAAG,UAAU;KAC7C;KACD;AACJ,OAAI,iBACF,QAAO;IAAE,SAAS,IAAI;IAAmB;IAAW;AAEtD,UAAO,EAAE,WAAW;;AAEtB,MAAI,iBACF,QAAO,EAAE,SAAS,IAAI,SAAmB;AAG3C,MAAI,MAAM,QAAQ,IAAI,QAAQ,IAAI,IAAI,QAAQ,SAAS,GAAG;GACxD,MAAM,QAAQ,IAAI,QAAQ;AAC1B,OAAI,OAAO,MAAM,SAAS,SACxB,QAAO,EAAE,SAAS,MAAM,MAAM;;;AASpC,KAAI,OAAO,IAAI,aAAa,YAAY,OAAO,IAAI,SAAS,UAC1D,QAAO,EAAE,SAAS,IAAI,UAAU;AAMlC,SAAQ,KACN,0DAA0D,OAAO,gDAAgD,OAAO,KACtH,IACD,CAAC,KAAK,KAAK,CAAC,GACd;AACD,QAAO;EACL,OAAO;GACL,SAAS;GACT,MAAM;GACP;EACD;EACD;;AAQH,SAAgB,kBACd,SACA,cASA;CACA,MAAM,QAQF,EAAE;AAGN,KAAI,QAAQ,iBAAiB,QAAQ,kBAAkB,OACrD,OAAM,WAAW,QAAQ;AAI3B,KAAI,QAAQ,gBAAgB;AAC1B,QAAM,YAAY,QAAQ;AAC1B,SAAO;;CAIT,MAAM,WAAW,qBAAqB,QAAQ,YAAY,EAAE,EAAE,OAAO;AACrE,KAAI,UAAU;EACZ,MAAM,OAAO,eAAe,SAAS,QAAQ;AAC7C,MAAI,KACF,OAAM,cAAc;;AAMxB,KAAI,QAAQ,MACV,OAAM,QACJ,mBAAmB,QAAQ,OAAO,cAAc,uBAAuB,IAAI,QAAQ;CAgBvF,MAAM,WAAW,QAAQ,YAAY,EAAE;AACvC,KACE,SAAS,SAAS,MACjB,QAAQ,kBAAkB,UAAU,QAAQ,kBAAkB,SAC/D;AACA,QAAM,YAAY,SAAS,QAAQ,MAAM,EAAE,SAAS,YAAY,CAAC;AACjE,QAAM,gBAAgB,yBAAyB,SAAS;;AAG1D,KAAI,QAAQ,SACV,OAAM,UAAU,QAAQ;AAG1B,QAAO;;;;;;;AAQT,SAAS,qBACP,SACyD;CACzD,MAAM,OAAoD,EAAE;CAG5D,MAAM,eADW,QAAQ,YAAY,EAAE,EAEpC,QAAQ,MAAM,EAAE,SAAS,SAAS,CAClC,KAAK,MAAO,OAAO,EAAE,YAAY,WAAW,EAAE,UAAU,KAAK,UAAU,EAAE,QAAQ,CAAE,CACnF,KAAK,KAAK;AACb,KAAI,YACF,MAAK,aAAaD,SAAO,WAAW,SAAS,CAAC,OAAO,YAAY,CAAC,OAAO,MAAM,CAAC,MAAM,GAAG,EAAE;AAG7F,KAAI,QAAQ,SAAS,QAAQ,MAAM,SAAS,EAC1C,MAAK,YAAYA,SACd,WAAW,SAAS,CACpB,OAAO,KAAK,UAAU,QAAQ,MAAM,CAAC,CACrC,OAAO,MAAM,CACb,MAAM,GAAG,EAAE;AAGhB,QAAO,OAAO,KAAK,KAAK,CAAC,SAAS,IAAI,OAAO;;;;;;;;;;AAuB/C,SAAgB,oBAAoB,MAKN;CAC5B,MAAM,EAAE,UAAU,OAAO,UAAU,WAAW;CAC9C,IAAI,WAA2C;CAC/C,IAAI,OAAsB;AAC1B,KAAI;AACF,SAAO,GAAG,aAAa,UAAU,QAAQ;UAClC,KAAK;AACZ,MAAK,IAA8B,SAAS,SAE1C,QAAO;GAAE,MAAM;GAAU,OAAO;GAAS,OAD7B,eAAe,QAAQ,IAAI,UAAU;GACI;;AAGzD,KAAI,SAAS,MAAM;EACjB,IAAI;AACJ,MAAI;AACF,YAAS,KAAK,MAAM,KAAK;UACnB;AACN,UAAO;IAAE,MAAM;IAAU,OAAO;IAAQ,OAAO,GAAG,SAAS;IAAqB;;AAElF,MAAI,WAAW,QAAQ,OAAO,WAAW,YAAY,MAAM,QAAQ,OAAO,CACxE,QAAO;GAAE,MAAM;GAAU,OAAO;GAAQ,OAAO,GAAG,SAAS;GAAwB;AAErF,aAAW;;CAEb,MAAM,UAAU,MAAM,SAAS;CAC/B,MAAM,UAAU,SAAS,QAAQ;AACjC,KAAI,YAAY,KAAM,QAAO;EAAE,MAAM;EAAU,OAAO;EAAY,OAAO;EAAS;CAElF,IAAI;AACJ,KAAI;AACF,KAAG,UAAU,KAAK,QAAQ,SAAS,EAAE,EAAE,WAAW,MAAM,CAAC;EACzD,MAAM,QAAQ,OAAO,KAAK,KAAK,UAAU,SAAS,MAAM,EAAE,EAAE,QAAQ;AACpE,YAAU,WAAW,UAAUA,SAAO,YAAY;AAClD,KAAG,cAAc,SAAS,OAAO;GAAE,MAAM;GAAM,MAAM;GAAO,CAAC;AAC7D,KAAG,WAAW,SAAS,SAAS;AAChC,YAAU;AACV,SAAO,KAAK,uBAAuB,WAAW;AAE9C,SAAO;GAAE,MAAM;GAAW;GAAU,QADrBA,SAAO,WAAW,SAAS,CAAC,OAAO,MAAM,CAAC,OAAO,MAAM;GAC1B;GAAS;UAC9C,KAAK;AAEZ,SAAO;GAAE,MAAM;GAAU,OAAO;GAAS,OAD7B,eAAe,QAAQ,IAAI,UAAU;GACI;WAC7C;AACR,MAAI,QACF,KAAI;AACF,MAAG,WAAW,QAAQ;UAChB"}