@copilotkit/aimock 1.28.0 → 1.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/bedrock-converse.cjs +63 -31
  3. package/dist/bedrock-converse.cjs.map +1 -1
  4. package/dist/bedrock-converse.d.cts.map +1 -1
  5. package/dist/bedrock-converse.d.ts.map +1 -1
  6. package/dist/bedrock-converse.js +65 -33
  7. package/dist/bedrock-converse.js.map +1 -1
  8. package/dist/bedrock.cjs +95 -33
  9. package/dist/bedrock.cjs.map +1 -1
  10. package/dist/bedrock.d.cts.map +1 -1
  11. package/dist/bedrock.d.ts.map +1 -1
  12. package/dist/bedrock.js +97 -35
  13. package/dist/bedrock.js.map +1 -1
  14. package/dist/cohere.cjs +49 -15
  15. package/dist/cohere.cjs.map +1 -1
  16. package/dist/cohere.d.cts.map +1 -1
  17. package/dist/cohere.d.ts.map +1 -1
  18. package/dist/cohere.js +51 -17
  19. package/dist/cohere.js.map +1 -1
  20. package/dist/config-loader.d.ts.map +1 -1
  21. package/dist/elevenlabs-audio.cjs +8 -4
  22. package/dist/elevenlabs-audio.cjs.map +1 -1
  23. package/dist/elevenlabs-audio.d.cts.map +1 -1
  24. package/dist/elevenlabs-audio.d.ts.map +1 -1
  25. package/dist/elevenlabs-audio.js +10 -6
  26. package/dist/elevenlabs-audio.js.map +1 -1
  27. package/dist/embeddings.cjs +4 -3
  28. package/dist/embeddings.cjs.map +1 -1
  29. package/dist/embeddings.d.cts.map +1 -1
  30. package/dist/embeddings.d.ts.map +1 -1
  31. package/dist/embeddings.js +6 -5
  32. package/dist/embeddings.js.map +1 -1
  33. package/dist/fal-audio.cjs +8 -4
  34. package/dist/fal-audio.cjs.map +1 -1
  35. package/dist/fal-audio.d.cts.map +1 -1
  36. package/dist/fal-audio.d.ts.map +1 -1
  37. package/dist/fal-audio.js +10 -6
  38. package/dist/fal-audio.js.map +1 -1
  39. package/dist/fal.cjs +4 -2
  40. package/dist/fal.cjs.map +1 -1
  41. package/dist/fal.d.cts.map +1 -1
  42. package/dist/fal.d.ts.map +1 -1
  43. package/dist/fal.js +6 -4
  44. package/dist/fal.js.map +1 -1
  45. package/dist/gemini-embeddings.cjs +4 -3
  46. package/dist/gemini-embeddings.cjs.map +1 -1
  47. package/dist/gemini-embeddings.js +6 -5
  48. package/dist/gemini-embeddings.js.map +1 -1
  49. package/dist/gemini-interactions.cjs +3 -3
  50. package/dist/gemini-interactions.cjs.map +1 -1
  51. package/dist/gemini-interactions.d.cts.map +1 -1
  52. package/dist/gemini-interactions.d.ts.map +1 -1
  53. package/dist/gemini-interactions.js +5 -5
  54. package/dist/gemini-interactions.js.map +1 -1
  55. package/dist/gemini.cjs +55 -24
  56. package/dist/gemini.cjs.map +1 -1
  57. package/dist/gemini.d.cts.map +1 -1
  58. package/dist/gemini.d.ts.map +1 -1
  59. package/dist/gemini.js +57 -26
  60. package/dist/gemini.js.map +1 -1
  61. package/dist/helpers.cjs +120 -2
  62. package/dist/helpers.cjs.map +1 -1
  63. package/dist/helpers.d.cts +43 -3
  64. package/dist/helpers.d.cts.map +1 -1
  65. package/dist/helpers.d.ts +43 -3
  66. package/dist/helpers.d.ts.map +1 -1
  67. package/dist/helpers.js +117 -3
  68. package/dist/helpers.js.map +1 -1
  69. package/dist/images.cjs +12 -6
  70. package/dist/images.cjs.map +1 -1
  71. package/dist/images.d.cts.map +1 -1
  72. package/dist/images.d.ts.map +1 -1
  73. package/dist/images.js +14 -8
  74. package/dist/images.js.map +1 -1
  75. package/dist/index.cjs +3 -0
  76. package/dist/index.d.cts +3 -3
  77. package/dist/index.d.ts +3 -3
  78. package/dist/index.js +3 -3
  79. package/dist/journal.cjs +10 -0
  80. package/dist/journal.cjs.map +1 -1
  81. package/dist/journal.d.cts +8 -0
  82. package/dist/journal.d.ts +8 -0
  83. package/dist/journal.js +10 -0
  84. package/dist/journal.js.map +1 -1
  85. package/dist/messages.cjs +325 -85
  86. package/dist/messages.cjs.map +1 -1
  87. package/dist/messages.d.cts.map +1 -1
  88. package/dist/messages.d.ts.map +1 -1
  89. package/dist/messages.js +327 -87
  90. package/dist/messages.js.map +1 -1
  91. package/dist/model-utils.cjs +68 -0
  92. package/dist/model-utils.cjs.map +1 -1
  93. package/dist/model-utils.js +68 -1
  94. package/dist/model-utils.js.map +1 -1
  95. package/dist/ollama.cjs +58 -21
  96. package/dist/ollama.cjs.map +1 -1
  97. package/dist/ollama.d.cts.map +1 -1
  98. package/dist/ollama.d.ts.map +1 -1
  99. package/dist/ollama.js +60 -23
  100. package/dist/ollama.js.map +1 -1
  101. package/dist/recorder.cjs +49 -8
  102. package/dist/recorder.cjs.map +1 -1
  103. package/dist/recorder.js +50 -9
  104. package/dist/recorder.js.map +1 -1
  105. package/dist/responses.cjs +26 -12
  106. package/dist/responses.cjs.map +1 -1
  107. package/dist/responses.d.cts +1 -1
  108. package/dist/responses.d.cts.map +1 -1
  109. package/dist/responses.d.ts +1 -1
  110. package/dist/responses.d.ts.map +1 -1
  111. package/dist/responses.js +28 -14
  112. package/dist/responses.js.map +1 -1
  113. package/dist/router.cjs +37 -8
  114. package/dist/router.cjs.map +1 -1
  115. package/dist/router.d.cts +30 -1
  116. package/dist/router.d.cts.map +1 -1
  117. package/dist/router.d.ts +30 -1
  118. package/dist/router.d.ts.map +1 -1
  119. package/dist/router.js +37 -9
  120. package/dist/router.js.map +1 -1
  121. package/dist/server.cjs +55 -19
  122. package/dist/server.cjs.map +1 -1
  123. package/dist/server.d.cts.map +1 -1
  124. package/dist/server.d.ts.map +1 -1
  125. package/dist/server.js +57 -21
  126. package/dist/server.js.map +1 -1
  127. package/dist/speech.cjs +4 -2
  128. package/dist/speech.cjs.map +1 -1
  129. package/dist/speech.d.cts.map +1 -1
  130. package/dist/speech.d.ts.map +1 -1
  131. package/dist/speech.js +6 -4
  132. package/dist/speech.js.map +1 -1
  133. package/dist/stream-collapse.cjs +44 -1
  134. package/dist/stream-collapse.cjs.map +1 -1
  135. package/dist/stream-collapse.d.cts +28 -0
  136. package/dist/stream-collapse.d.cts.map +1 -1
  137. package/dist/stream-collapse.d.ts +28 -0
  138. package/dist/stream-collapse.d.ts.map +1 -1
  139. package/dist/stream-collapse.js +44 -2
  140. package/dist/stream-collapse.js.map +1 -1
  141. package/dist/transcription.cjs +4 -2
  142. package/dist/transcription.cjs.map +1 -1
  143. package/dist/transcription.d.cts.map +1 -1
  144. package/dist/transcription.d.ts.map +1 -1
  145. package/dist/transcription.js +6 -4
  146. package/dist/transcription.js.map +1 -1
  147. package/dist/types.d.cts +42 -0
  148. package/dist/types.d.cts.map +1 -1
  149. package/dist/types.d.ts +42 -0
  150. package/dist/types.d.ts.map +1 -1
  151. package/dist/vector-types.d.cts.map +1 -1
  152. package/dist/vector-types.d.ts.map +1 -1
  153. package/dist/video.cjs +21 -3
  154. package/dist/video.cjs.map +1 -1
  155. package/dist/video.d.cts.map +1 -1
  156. package/dist/video.d.ts.map +1 -1
  157. package/dist/video.js +23 -5
  158. package/dist/video.js.map +1 -1
  159. package/dist/ws-gemini-live.cjs +4 -3
  160. package/dist/ws-gemini-live.cjs.map +1 -1
  161. package/dist/ws-gemini-live.d.cts.map +1 -1
  162. package/dist/ws-gemini-live.d.ts.map +1 -1
  163. package/dist/ws-gemini-live.js +6 -5
  164. package/dist/ws-gemini-live.js.map +1 -1
  165. package/dist/ws-realtime.cjs +4 -3
  166. package/dist/ws-realtime.cjs.map +1 -1
  167. package/dist/ws-realtime.d.cts.map +1 -1
  168. package/dist/ws-realtime.d.ts.map +1 -1
  169. package/dist/ws-realtime.js +6 -5
  170. package/dist/ws-realtime.js.map +1 -1
  171. package/dist/ws-responses.cjs +8 -6
  172. package/dist/ws-responses.cjs.map +1 -1
  173. package/dist/ws-responses.d.cts.map +1 -1
  174. package/dist/ws-responses.d.ts.map +1 -1
  175. package/dist/ws-responses.js +10 -8
  176. package/dist/ws-responses.js.map +1 -1
  177. package/package.json +2 -2
package/dist/recorder.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { DEFAULT_TEST_ID } from "./constants.js";
2
+ import { normalizeModelName } from "./model-utils.js";
2
3
  import { getTestId, slugifyTestId } from "./helpers.js";
3
4
  import { getLastMessageByRole, getTextContent } from "./router.js";
4
5
  import { writeErrorResponse } from "./sse-writer.js";
5
- import { normalizeModelName } from "./model-utils.js";
6
- import { collapseStreamingResponse } from "./stream-collapse.js";
6
+ import { capturedRedactedData, collapseStreamingResponse } from "./stream-collapse.js";
7
7
  import { resolveUpstreamUrl } from "./url.js";
8
8
  import * as http$1 from "node:http";
9
9
  import * as crypto$1 from "node:crypto";
@@ -192,14 +192,22 @@ async function proxyAndRecord(req, res, request, providerKey, pathname, fixtures
192
192
  } else if (collapsed.content === "" && (!collapsed.toolCalls || collapsed.toolCalls.length === 0)) {
193
193
  defaults.logger.warn("Stream collapse produced empty content — fixture may be incomplete");
194
194
  const reasoningSpread = collapsed.reasoning ? { reasoning: collapsed.reasoning } : {};
195
+ const reasoningSignatureSpread = collapsed.reasoning && collapsed.reasoningSignature ? { reasoningSignature: collapsed.reasoningSignature } : {};
196
+ logDroppedReasoningSignature(defaults.logger, collapsed.reasoning, collapsed.reasoningSignature);
197
+ const redactedThinkingSpread = collapsed.redactedThinking?.length ? { redactedThinking: collapsed.redactedThinking } : {};
195
198
  const webSearchesSpread = collapsed.webSearches?.length ? { webSearches: collapsed.webSearches } : {};
196
199
  fixtureResponse = {
197
200
  content: collapsed.content ?? "",
198
201
  ...reasoningSpread,
202
+ ...reasoningSignatureSpread,
203
+ ...redactedThinkingSpread,
199
204
  ...webSearchesSpread
200
205
  };
201
206
  } else {
202
207
  const reasoningSpread = collapsed.reasoning ? { reasoning: collapsed.reasoning } : {};
208
+ const reasoningSignatureSpread = collapsed.reasoning && collapsed.reasoningSignature ? { reasoningSignature: collapsed.reasoningSignature } : {};
209
+ logDroppedReasoningSignature(defaults.logger, collapsed.reasoning, collapsed.reasoningSignature);
210
+ const redactedThinkingSpread = collapsed.redactedThinking?.length ? { redactedThinking: collapsed.redactedThinking } : {};
203
211
  const webSearchesSpread = collapsed.webSearches?.length ? { webSearches: collapsed.webSearches } : {};
204
212
  if (collapsed.toolCalls && collapsed.toolCalls.length > 0) {
205
213
  const sanitizedToolCalls = collapsed.toolCalls.map((tc) => ({
@@ -211,16 +219,22 @@ async function proxyAndRecord(req, res, request, providerKey, pathname, fixtures
211
219
  content: collapsed.content,
212
220
  toolCalls: sanitizedToolCalls,
213
221
  ...reasoningSpread,
222
+ ...reasoningSignatureSpread,
223
+ ...redactedThinkingSpread,
214
224
  ...webSearchesSpread
215
225
  };
216
226
  else fixtureResponse = {
217
227
  toolCalls: sanitizedToolCalls,
218
228
  ...reasoningSpread,
229
+ ...reasoningSignatureSpread,
230
+ ...redactedThinkingSpread,
219
231
  ...webSearchesSpread
220
232
  };
221
233
  } else fixtureResponse = {
222
234
  content: collapsed.content ?? "",
223
235
  ...reasoningSpread,
236
+ ...reasoningSignatureSpread,
237
+ ...redactedThinkingSpread,
224
238
  ...webSearchesSpread
225
239
  };
226
240
  }
@@ -255,7 +269,7 @@ async function proxyAndRecord(req, res, request, providerKey, pathname, fixtures
255
269
  } catch (err) {
256
270
  defaults.logger.debug(`Could not parse encoding_format from raw body: ${err instanceof Error ? err.message : "unknown error"}`);
257
271
  }
258
- fixtureResponse = buildFixtureResponse(parsedResponse, upstreamStatus, encodingFormat);
272
+ fixtureResponse = buildFixtureResponse(parsedResponse, upstreamStatus, encodingFormat, defaults.logger);
259
273
  }
260
274
  }
261
275
  if (clientDisconnected) {
@@ -461,10 +475,19 @@ function makeUpstreamRequest(target, headers, body, clientRes, method = "POST",
461
475
  });
462
476
  }
463
477
  /**
478
+ * A captured Anthropic thinking-block signature is only persisted alongside
479
+ * non-empty plaintext `reasoning` (a bare signature has nothing to attach to on
480
+ * replay). When that gate drops a present signature, warn so the loss is
481
+ * observable (matching the recording-side anomaly convention in this file).
482
+ */
483
+ function logDroppedReasoningSignature(logger, reasoning, reasoningSignature) {
484
+ if (reasoningSignature && !reasoning) logger?.warn("Dropping captured reasoningSignature — no plaintext reasoning to attach it to");
485
+ }
486
+ /**
464
487
  * Detect the response format from the parsed upstream JSON and convert
465
488
  * it into an aimock FixtureResponse.
466
489
  */
467
- function buildFixtureResponse(parsed, status, encodingFormat) {
490
+ function buildFixtureResponse(parsed, status, encodingFormat, logger) {
468
491
  if (parsed === null || parsed === void 0) return {
469
492
  error: {
470
493
  message: "Upstream returned non-JSON response",
@@ -593,10 +616,20 @@ function buildFixtureResponse(parsed, status, encodingFormat) {
593
616
  const toolUseBlocks = blocks.filter((b) => b.type === "tool_use");
594
617
  const textBlocks = blocks.filter((b) => b.type === "text" && typeof b.text === "string");
595
618
  const thinkingBlocks = blocks.filter((b) => b.type === "thinking");
619
+ const redactedBlocks = blocks.filter((b) => b.type === "redacted_thinking");
620
+ const redactedThinking = blocks.map((b) => capturedRedactedData(b)).filter((data) => data !== void 0);
596
621
  const hasToolCalls = toolUseBlocks.length > 0;
597
622
  const joinedText = textBlocks.map((b) => String(b.text ?? "")).join("");
598
623
  const hasContent = joinedText.length > 0;
599
624
  const anthropicReasoning = thinkingBlocks.length > 0 ? thinkingBlocks.map((b) => String(b.thinking ?? "")).join("") : void 0;
625
+ const anthropicReasoningSignature = (() => {
626
+ let sig;
627
+ for (const b of thinkingBlocks) if (typeof b.signature === "string") sig = String(b.signature);
628
+ return sig;
629
+ })();
630
+ const reasoningSignatureSpread = anthropicReasoning && anthropicReasoningSignature ? { reasoningSignature: anthropicReasoningSignature } : {};
631
+ logDroppedReasoningSignature(logger, anthropicReasoning, anthropicReasoningSignature);
632
+ const redactedThinkingSpread = redactedThinking.length > 0 ? { redactedThinking } : {};
600
633
  if (hasToolCalls) {
601
634
  const toolCalls = toolUseBlocks.map((b) => ({
602
635
  name: String(b.name),
@@ -606,20 +639,28 @@ function buildFixtureResponse(parsed, status, encodingFormat) {
606
639
  if (hasContent) return {
607
640
  content: joinedText,
608
641
  toolCalls,
609
- ...anthropicReasoning ? { reasoning: anthropicReasoning } : {}
642
+ ...anthropicReasoning ? { reasoning: anthropicReasoning } : {},
643
+ ...reasoningSignatureSpread,
644
+ ...redactedThinkingSpread
610
645
  };
611
646
  return {
612
647
  toolCalls,
613
- ...anthropicReasoning ? { reasoning: anthropicReasoning } : {}
648
+ ...anthropicReasoning ? { reasoning: anthropicReasoning } : {},
649
+ ...reasoningSignatureSpread,
650
+ ...redactedThinkingSpread
614
651
  };
615
652
  }
616
653
  if (hasContent) return {
617
654
  content: joinedText,
618
- ...anthropicReasoning ? { reasoning: anthropicReasoning } : {}
655
+ ...anthropicReasoning ? { reasoning: anthropicReasoning } : {},
656
+ ...reasoningSignatureSpread,
657
+ ...redactedThinkingSpread
619
658
  };
620
- if (anthropicReasoning) return {
659
+ if (thinkingBlocks.length > 0 || redactedBlocks.length > 0) return {
621
660
  content: "",
622
- reasoning: anthropicReasoning
661
+ ...anthropicReasoning ? { reasoning: anthropicReasoning } : {},
662
+ ...reasoningSignatureSpread,
663
+ ...redactedThinkingSpread
623
664
  };
624
665
  }
625
666
  if (Array.isArray(obj.candidates) && obj.candidates.length > 0) {
@@ -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 ToolCall,\n} from \"./types.js\";\nimport { getLastMessageByRole, getTextContent } from \"./router.js\";\nimport { normalizeModelName } from \"./model-utils.js\";\nimport type { Logger } from \"./logger.js\";\nimport { collapseStreamingResponse } from \"./stream-collapse.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport { resolveUpstreamUrl } from \"./url.js\";\nimport { getTestId, slugifyTestId } from \"./helpers.js\";\nimport { DEFAULT_TEST_ID } from \"./constants.js\";\n\n/** Headers to strip when proxying — hop-by-hop (RFC 2616 §13.5.1) + client-set. */\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]);\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 * 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 fixture: Fixture;\n fixtures: Fixture[];\n warnings?: string[];\n logger: Logger;\n}): PersistFixtureResult {\n const { record, providerKey, testId, fixture, fixtures, warnings = [], logger } = opts;\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 if (isEmptyMatch) {\n logger.warn(\"Recorded fixture has empty match criteria — skipping in-memory registration\");\n }\n\n if (record.proxyOnly) {\n logger.info(`Proxied ${providerKey} request (proxy-only mode)`);\n return { kind: \"skipped\" };\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 `${providerKey}-${timestamp}-${crypto.randomUUID().slice(0, 8)}.json`,\n );\n } else {\n filepath = path.join(fixturePath, slug, `${providerKey}.json`);\n mergeExisting = true;\n }\n } else {\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const timestampFile = `${providerKey}-${timestamp}-${crypto.randomUUID().slice(0, 8)}.json`;\n filepath = fixture.match.context\n ? path.join(fixturePath, fixture.match.context, 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 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 let fileContent: { fixtures: unknown[]; _warning?: string };\n if (mergeExisting && fs.existsSync(filepath)) {\n try {\n const existing = JSON.parse(fs.readFileSync(filepath, \"utf-8\"));\n fileContent = { fixtures: [...(existing.fixtures ?? []), fixture] };\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 fileContent._warning = fileWarnings.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 const tmpPath = filepath + \".tmp.\" + process.pid;\n fs.writeFileSync(tmpPath, JSON.stringify(fileContent, null, 2), \"utf-8\");\n fs.renameSync(tmpPath, filepath);\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 }\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: Record<string, string> = {};\n for (const [name, val] of Object.entries(req.headers)) {\n if (val !== undefined && !STRIP_HEADERS.has(name)) {\n forwardHeaders[name] = Array.isArray(val) ? val.join(\", \") : val;\n }\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\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 frameTimestamps: number[] = [];\n let streamStartTime = 0;\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 );\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 frameTimestamps = result.frameTimestamps;\n streamStartTime = result.streamStartTime;\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 // SSE headers already sent — gracefully close the connection\n res.end();\n }\n return \"relayed\";\n }\n\n // Detect streaming response and collapse if necessary.\n // NOTE: collapse buffers the entire upstream body in memory. Fine for\n // current chat-completions traffic (responses are small), but revisit if\n // this path ever proxies long-lived or large streams — both the buffer\n // here and the hook below receive the full payload.\n const contentType = upstreamHeaders[\"content-type\"];\n const ctString = Array.isArray(contentType) ? contentType.join(\", \") : (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\n const isAudioResponse = ctString.toLowerCase().startsWith(\"audio/\");\n if (isAudioResponse && rawBuffer.length > 0) {\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 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 // 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.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 const webSearchesSpread = collapsed.webSearches?.length\n ? { webSearches: collapsed.webSearches }\n : {};\n fixtureResponse = {\n content: collapsed.content ?? \"\",\n ...reasoningSpread,\n ...webSearchesSpread,\n };\n } else {\n const reasoningSpread = collapsed.reasoning ? { reasoning: collapsed.reasoning } : {};\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 fixtureResponse = {\n content: collapsed.content,\n toolCalls: sanitizedToolCalls,\n ...reasoningSpread,\n ...webSearchesSpread,\n };\n } else {\n fixtureResponse = {\n toolCalls: sanitizedToolCalls,\n ...reasoningSpread,\n ...webSearchesSpread,\n };\n }\n } else {\n fixtureResponse = {\n content: collapsed.content ?? \"\",\n ...reasoningSpread,\n ...webSearchesSpread,\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 returns arbitrary, model-specific JSON shapes (images, video URLs,\n // audio file objects, etc.). Round-trip the payload verbatim instead of\n // letting buildFixtureResponse mis-classify it as ImageResponse / VideoResponse.\n if (request._endpointType === \"fal\" && parsedResponse !== null) {\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 let encodingFormat: string | undefined;\n try {\n encodingFormat = rawBody ? JSON.parse(rawBody).encoding_format : undefined;\n } catch (err) {\n defaults.logger.debug(\n `Could not parse encoding_format from raw body: ${err instanceof Error ? err.message : \"unknown error\"}`,\n );\n }\n fixtureResponse = buildFixtureResponse(parsedResponse, upstreamStatus, encodingFormat);\n }\n }\n\n // If the client disconnected mid-stream, the collected data is likely\n // truncated. Saving a partial fixture is worse than saving none — skip\n // fixture persistence entirely.\n if (clientDisconnected) {\n defaults.logger.warn(\n \"Client disconnected mid-stream — skipping fixture save to avoid truncated data\",\n );\n return \"relayed\";\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\", 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\nfunction clampTimeout(value: number | undefined, fallback: number): number {\n if (value == null || !Number.isFinite(value) || value <= 0) return fallback;\n return 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): 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 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 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 = Array.isArray(ct) ? ct.join(\", \") : (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 const streamStartTime = 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 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 // Stop relaying if the client disconnects mid-stream.\n // Check writableFinished to distinguish normal completion (where\n // \"close\" also fires) from premature client disconnects.\n clientRes.on(\"close\", () => {\n if (!clientRes.writableFinished) {\n clientDisconnected = true;\n req.destroy();\n }\n });\n }\n const chunks: Buffer[] = [];\n res.on(\"data\", (chunk: Buffer) => {\n chunks.push(chunk);\n\n // Capture per-frame timestamps for SSE/NDJSON streams.\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 (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).\n for (let fi = 0; fi < parts.length - 1; fi++) {\n if (parts[fi].trim().length > 0) {\n frameTimestamps.push(Date.now());\n }\n }\n // Last part stays in buffer (may be incomplete)\n frameBuffer = parts[parts.length - 1];\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 if (isBinaryEventStream) {\n binaryFrameBuffer = Buffer.concat([binaryFrameBuffer, chunk]);\n while (binaryFrameBuffer.length >= 4) {\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 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(\"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.\n if (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 resolve({\n status: res.statusCode ?? 500,\n headers: res.headers,\n body: rawBuffer.toString(),\n rawBuffer,\n streamedToClient,\n clientDisconnected,\n frameTimestamps,\n streamStartTime,\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 req.write(body);\n req.end();\n });\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 encodingFormat?: string,\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 if (typeof first.embedding === \"string\" && encodingFormat === \"base64\") {\n const buf = Buffer.from(first.embedding, \"base64\");\n if (buf.byteLength % 4 !== 0) {\n // Malformed embedding — return a zero-dimension embedding fixture\n return { embedding: [] };\n }\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 // OpenAI image generation: { created, data: [{ url, b64_json, revised_prompt }] }\n if (first.url || first.b64_json) {\n const images = (obj.data as Array<Record<string, unknown>>).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 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: \"...\", ... }\n if (\n typeof obj.text === \"string\" &&\n (obj.task === \"transcribe\" || obj.language !== undefined || obj.duration !== undefined)\n ) {\n return {\n transcription: {\n text: obj.text as string,\n ...(obj.language ? { language: String(obj.language) } : {}),\n ...(obj.duration !== undefined ? { duration: Number(obj.duration) } : {}),\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: typeof o.arguments === \"string\" ? o.arguments : JSON.stringify(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 const openaiReasoning =\n typeof message.reasoning_content === \"string\" && message.reasoning_content.length > 0\n ? message.reasoning_content\n : undefined;\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: String(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 };\n }\n return { toolCalls, ...(openaiReasoning ? { reasoning: openaiReasoning } : {}) };\n }\n // Text content only\n if (hasContent) {\n return {\n content: message.content as string,\n ...(openaiReasoning ? { reasoning: openaiReasoning } : {}),\n };\n }\n // Recognized OpenAI shape but empty content (e.g. content filtering, zero max_tokens)\n return { content: \"\", ...(openaiReasoning ? { reasoning: openaiReasoning } : {}) };\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 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\n if (hasToolCalls) {\n const toolCalls: ToolCall[] = toolUseBlocks.map((b) => ({\n name: String(b.name),\n arguments: typeof b.input === \"string\" ? b.input : JSON.stringify(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 };\n }\n return { toolCalls, ...(anthropicReasoning ? { reasoning: anthropicReasoning } : {}) };\n }\n if (hasContent) {\n return {\n content: joinedText,\n ...(anthropicReasoning ? { reasoning: anthropicReasoning } : {}),\n };\n }\n // Thinking-only response (no text, no tool calls)\n if (anthropicReasoning) {\n return { content: \"\", reasoning: anthropicReasoning };\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\n const audioParts = parts.filter(\n (p: Record<string, unknown>) =>\n p.inlineData &&\n typeof (p.inlineData as Record<string, unknown>).mimeType === \"string\" &&\n ((p.inlineData as Record<string, unknown>).mimeType as string).startsWith(\"audio/\"),\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: String(inlineData.mimeType),\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: typeof fc.args === \"string\" ? fc.args : JSON.stringify(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: typeof tu.input === \"string\" ? tu.input : JSON.stringify(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 const textBlock = contentBlocks.find((b) => b.type === \"text\" && typeof b.text === \"string\");\n const hasContent = textBlock && typeof textBlock.text === \"string\" && textBlock.text.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 : JSON.stringify((b.function as Record<string, unknown>)?.arguments),\n ...(b.id ? { id: String(b.id) } : {}),\n }));\n if (hasContent) {\n return { content: textBlock.text as string, 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 : JSON.stringify(fn?.arguments),\n ...(tc.id ? { id: String(tc.id) } : {}),\n };\n });\n if (hasContent) {\n return { content: textBlock.text as string, toolCalls };\n }\n return { toolCalls };\n }\n if (hasContent) {\n return { content: textBlock.text as string };\n }\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:\n typeof fn.arguments === \"string\" ? fn.arguments : JSON.stringify(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 if (typeof obj.response === \"string\" && \"done\" in obj) {\n return { content: obj.response };\n }\n\n // Fallback: unknown format — save as error\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 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 = messages.some((m) => m.role === \"tool\");\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":";;;;;;;;;;;;;;;;AA0BA,MAAM,gBAAgB,IAAI,IAAI;CAE5B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CAEA;CACA;CACD,CAAC;;;;;;;;;AA4EF,SAAgB,eAAe,MAQN;CACvB,MAAM,EAAE,QAAQ,aAAa,QAAQ,SAAS,UAAU,WAAW,EAAE,EAAE,WAAW;CAMlF,MAAM,IAAI,QAAQ;CAClB,MAAM,eACJ,EAAE,gBAAgB,UAAa,EAAE,cAAc,UAAa,EAAE,aAAa;AAC7E,KAAI,aACF,QAAO,KAAK,8EAA8E;AAG5F,KAAI,OAAO,WAAW;AACpB,SAAO,KAAK,WAAW,YAAY,4BAA4B;AAC/D,SAAO,EAAE,MAAM,WAAW;;CAG5B,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,YAAY,GAAG,UAAU,GAAGA,SAAO,YAAY,CAAC,MAAM,GAAG,EAAE,CAAC,OAChE;SACI;AACL,cAAW,KAAK,KAAK,aAAa,MAAM,GAAG,YAAY,OAAO;AAC9D,mBAAgB;;QAEb;EAEL,MAAM,gBAAgB,GAAG,YAAY,oBADnB,IAAI,MAAM,EAAC,aAAa,CAAC,QAAQ,SAAS,IAAI,CACd,GAAGA,SAAO,YAAY,CAAC,MAAM,GAAG,EAAE,CAAC;AACrF,aAAW,QAAQ,MAAM,UACrB,KAAK,KAAK,aAAa,QAAQ,MAAM,SAAS,cAAc,GAC5D,KAAK,KAAK,aAAa,cAAc;;CAG3C,MAAM,eAAe,CACnB,GAAI,eAAe,CAAC,iEAAiE,GAAG,EAAE,EAC1F,GAAG,SACJ;AAED,KAAI;AACF,KAAG,UAAU,KAAK,QAAQ,SAAS,EAAE,EAAE,WAAW,MAAM,CAAC;EAKzD,IAAI;AACJ,MAAI,iBAAiB,GAAG,WAAW,SAAS,CAC1C,KAAI;AAEF,iBAAc,EAAE,UAAU,CAAC,GADV,KAAK,MAAM,GAAG,aAAa,UAAU,QAAQ,CAAC,CACvB,YAAY,EAAE,EAAG,QAAQ,EAAE;WAC5D,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,EACxB,aAAY,WAAW,aAAa,KAAK,KAAK;EAMhD,MAAM,UAAU,WAAW,UAAU,QAAQ;AAC7C,KAAG,cAAc,SAAS,KAAK,UAAU,aAAa,MAAM,EAAE,EAAE,QAAQ;AACxE,KAAG,WAAW,SAAS,SAAS;AAEhC,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;;;;;;;;AASzC,eAAsB,eACpB,KACA,KACA,SACA,aACA,UACA,UACA,UAKA,SACA,SACuB;CACvB,MAAM,SAAS,SAAS;AACxB,KAAI,CAAC,OAAQ,QAAO;CAMpB,MAAM,cAJY,OAAO,UAGP,gBAAgB,wBAAwB,WAAW;AAGrE,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,iBAAyC,EAAE;AACjD,MAAK,MAAM,CAAC,MAAM,QAAQ,OAAO,QAAQ,IAAI,QAAQ,CACnD,KAAI,QAAQ,UAAa,CAAC,cAAc,IAAI,KAAK,CAC/C,gBAAe,QAAQ,MAAM,QAAQ,IAAI,GAAG,IAAI,KAAK,KAAK,GAAG;CAIjE,MAAM,cAAc,WAAW,KAAK,UAAU,QAAQ;CAGtD,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CAIJ,IAAI,mBAAmB;CACvB,IAAI,qBAAqB;CACzB,IAAI,kBAA4B,EAAE;CAClC,IAAI,kBAAkB;AACtB,KAAI;EACF,MAAM,SAAS,MAAM,oBACnB,QACA,gBACA,aACA,KACA,IAAI,QACJ,SAAS,QACT;GAAE,mBAAmB,OAAO;GAAmB,eAAe,OAAO;GAAe,CACrF;AACD,mBAAiB,OAAO;AACxB,oBAAkB,OAAO;AACzB,iBAAe,OAAO;AACtB,cAAY,OAAO;AACnB,qBAAmB,OAAO;AAC1B,uBAAqB,OAAO;AAC5B,oBAAkB,OAAO;AACzB,oBAAkB,OAAO;UAClB,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;QAGD,KAAI,KAAK;AAEX,SAAO;;CAQT,MAAM,cAAc,gBAAgB;CACpC,MAAM,WAAW,MAAM,QAAQ,YAAY,GAAG,YAAY,KAAK,KAAK,GAAI,eAAe;CACvF,MAAM,iBAAiB,SAAS,aAAa,CAAC,SAAS,qCAAqC;CAC5F,MAAM,YAAY,0BAChB,UACA,aACA,iBAAiB,YAAY,cAC7B,SAAS,OACV;CAED,IAAI;AAIJ,KADwB,SAAS,aAAa,CAAC,WAAW,SAAS,IAC5C,UAAU,SAAS,GAAG;EAE3C,MAAM,cAAc,SACjB,aAAa,CACb,QAAQ,UAAU,GAAG,CACrB,QAAQ,QAAQ,MAAM,CACtB,MAAM,IAAI,CAAC,GACX,MAAM;AACT,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;AAMH,MAAI,UAAU,UAAU;GACtB,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,IACxD;AACA,YAAS,OAAO,KAAK,qEAAqE;GAC1F,MAAM,kBAAkB,UAAU,YAAY,EAAE,WAAW,UAAU,WAAW,GAAG,EAAE;GACrF,MAAM,oBAAoB,UAAU,aAAa,SAC7C,EAAE,aAAa,UAAU,aAAa,GACtC,EAAE;AACN,qBAAkB;IAChB,SAAS,UAAU,WAAW;IAC9B,GAAG;IACH,GAAG;IACJ;SACI;GACL,MAAM,kBAAkB,UAAU,YAAY,EAAE,WAAW,UAAU,WAAW,GAAG,EAAE;GACrF,MAAM,oBAAoB,UAAU,aAAa,SAC7C,EAAE,aAAa,UAAU,aAAa,GACtC,EAAE;AACN,OAAI,UAAU,aAAa,UAAU,UAAU,SAAS,GAAG;IACzD,MAAM,qBAAqB,UAAU,UAAU,KAAK,QAAQ;KAC1D,GAAG;KACH,MAAM,GAAG,QAAQ;KACjB,WAAW,GAAG,aAAa;KAC5B,EAAE;AACH,QAAI,UAAU,QAEZ,mBAAkB;KAChB,SAAS,UAAU;KACnB,WAAW;KACX,GAAG;KACH,GAAG;KACJ;QAED,mBAAkB;KAChB,WAAW;KACX,GAAG;KACH,GAAG;KACJ;SAGH,mBAAkB;IAChB,SAAS,UAAU,WAAW;IAC9B,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;;AAKH,MAAI,QAAQ,kBAAkB,SAAS,mBAAmB,MAAM;GAC9D,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;SAE/D;GACL,IAAI;AACJ,OAAI;AACF,qBAAiB,UAAU,KAAK,MAAM,QAAQ,CAAC,kBAAkB;YAC1D,KAAK;AACZ,aAAS,OAAO,MACd,kDAAkD,eAAe,QAAQ,IAAI,UAAU,kBACxF;;AAEH,qBAAkB,qBAAqB,gBAAgB,gBAAgB,eAAe;;;AAO1F,KAAI,oBAAoB;AACtB,WAAS,OAAO,KACd,iFACD;AACD,SAAO;;CAMT,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,cAAc,MAAM;MAE3D,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;;;AAI7B,SAAS,aAAa,OAA2B,UAA0B;AACzE,KAAI,SAAS,QAAQ,CAAC,OAAO,SAAS,MAAM,IAAI,SAAS,EAAG,QAAO;AACnE,QAAO;;AAGT,SAAS,oBACP,QACA,SACA,MACA,WACA,SAAiB,QACjB,QACA,UAUC;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;EACrE,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,MAAM,QAAQ,GAAG,GAAG,GAAG,KAAK,KAAK,GAAI,MAAM;GACzD,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;GACpC,MAAM,kBAAkB,KAAK,KAAK;GAClC,IAAI,cAAc;GAIlB,MAAM,eAAe,IAAI,uBAAuB;GAChD,IAAI,oBAAoB,OAAO,MAAM,EAAE;GAEvC,IAAI,mBAAmB;GACvB,IAAI,qBAAqB;AACzB,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;AAInB,cAAU,GAAG,eAAe;AAC1B,SAAI,CAAC,UAAU,kBAAkB;AAC/B,2BAAqB;AACrB,UAAI,SAAS;;MAEf;;GAEJ,MAAM,SAAmB,EAAE;AAC3B,OAAI,GAAG,SAAS,UAAkB;AAChC,WAAO,KAAK,MAAM;AAKlB,QAAI,SAAS,UAAU;AACrB,oBAAe,aAAa,MAAM,MAAM;KAOxC,MAAM,YAAY,WAAW,UAAU;KACvC,MAAM,QAAQ,YAAY,MAAM,UAAU;AAG1C,UAAK,IAAI,KAAK,GAAG,KAAK,MAAM,SAAS,GAAG,KACtC,KAAI,MAAM,IAAI,MAAM,CAAC,SAAS,EAC5B,iBAAgB,KAAK,KAAK,KAAK,CAAC;AAIpC,mBAAc,MAAM,MAAM,SAAS;;AAMrC,QAAI,qBAAqB;AACvB,yBAAoB,OAAO,OAAO,CAAC,mBAAmB,MAAM,CAAC;AAC7D,YAAO,kBAAkB,UAAU,GAAG;MACpC,MAAM,WAAW,kBAAkB,aAAa,EAAE;AAClD,UAAI,WAAW,MAAM,kBAAkB,SAAS,SAAU;AAC1D,sBAAgB,KAAK,KAAK,KAAK,CAAC;AAChC,0BAAoB,kBAAkB,SAAS,SAAS;;;AAI5D,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;;KAGzB;AACF,OAAI,GAAG,SAAS,OAAO;AACvB,OAAI,GAAG,aAAa;AAClB,QAAI,IAAI,OAAQ,KAAI,WAAW,EAAE;AAKjC,QAAI,SAAS,UAAU;AAGrB,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;;AAGL,YAAQ;KACN,QAAQ,IAAI,cAAc;KAC1B,SAAS,IAAI;KACb,MAAM,UAAU,UAAU;KAC1B;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;AACvB,MAAI,MAAM,KAAK;AACf,MAAI,KAAK;GACT;;;;;;AAOJ,SAAS,qBACP,QACA,QACA,gBACiB;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;AAEnD,MAAI,OAAO,MAAM,cAAc,YAAY,mBAAmB,UAAU;GACtE,MAAM,MAAM,OAAO,KAAK,MAAM,WAAW,SAAS;AAClD,OAAI,IAAI,aAAa,MAAM,EAEzB,QAAO,EAAE,WAAW,EAAE,EAAE;GAI1B,MAAM,SAAS,IAAI,WAAW,IAAI;GAClC,MAAM,SAAS,IAAI,aAAa,OAAO,QAAQ,GAAG,IAAI,aAAa,EAAE;AACrE,UAAO,EAAE,WAAW,MAAM,KAAK,OAAO,EAAE;;AAG1C,MAAI,MAAM,OAAO,MAAM,UAAU;GAC/B,MAAM,SAAU,IAAI,KAAwC,KAAK,UAAU;IACzE,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;AACH,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;;AAInB,KACE,OAAO,IAAI,SAAS,aACnB,IAAI,SAAS,gBAAgB,IAAI,aAAa,UAAa,IAAI,aAAa,QAE7E,QAAO,EACL,eAAe;EACb,MAAM,IAAI;EACV,GAAI,IAAI,WAAW,EAAE,UAAU,OAAO,IAAI,SAAS,EAAE,GAAG,EAAE;EAC1D,GAAI,IAAI,aAAa,SAAY,EAAE,UAAU,OAAO,IAAI,SAAS,EAAE,GAAG,EAAE;EACxE,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,OAAO,EAAE,cAAc,WAAW,EAAE,YAAY,KAAK,UAAU,EAAE,UAAU;IACtF,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;GAEnF,MAAM,kBACJ,OAAO,QAAQ,sBAAsB,YAAY,QAAQ,kBAAkB,SAAS,IAChF,QAAQ,oBACR;AAEN,OAAI,cAAc;IAChB,MAAM,YAAyB,QAAQ,WAA8C,KAClF,OAAO;KACN,MAAM,KAAK,GAAG;AACd,YAAO;MACL,MAAM,OAAO,GAAG,KAAK;MACrB,WAAW,OAAO,GAAG,UAAU;MAC/B,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;KAC1D;AAEH,WAAO;KAAE;KAAW,GAAI,kBAAkB,EAAE,WAAW,iBAAiB,GAAG,EAAE;KAAG;;AAGlF,OAAI,WACF,QAAO;IACL,SAAS,QAAQ;IACjB,GAAI,kBAAkB,EAAE,WAAW,iBAAiB,GAAG,EAAE;IAC1D;AAGH,UAAO;IAAE,SAAS;IAAI,GAAI,kBAAkB,EAAE,WAAW,iBAAiB,GAAG,EAAE;IAAG;;;AAKtF,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;EAClE,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;AAEN,MAAI,cAAc;GAChB,MAAM,YAAwB,cAAc,KAAK,OAAO;IACtD,MAAM,OAAO,EAAE,KAAK;IACpB,WAAW,OAAO,EAAE,UAAU,WAAW,EAAE,QAAQ,KAAK,UAAU,EAAE,MAAM;IAC1E,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;IAChE;AAEH,UAAO;IAAE;IAAW,GAAI,qBAAqB,EAAE,WAAW,oBAAoB,GAAG,EAAE;IAAG;;AAExF,MAAI,WACF,QAAO;GACL,SAAS;GACT,GAAI,qBAAqB,EAAE,WAAW,oBAAoB,GAAG,EAAE;GAChE;AAGH,MAAI,mBACF,QAAO;GAAE,SAAS;GAAI,WAAW;GAAoB;;AAKzD,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;GAGtB,MAAM,aAAa,MAAM,QACtB,MACC,EAAE,cACF,OAAQ,EAAE,WAAuC,aAAa,YAC5D,EAAE,WAAuC,SAAoB,WAAW,SAAS,CACtF;AACD,OAAI,WAAW,SAAS,GAAG;IACzB,MAAM,aAAa,WAAW,GAAG;AACjC,WAAO,EACL,OAAO;KACL,SAAS,OAAO,WAAW,QAAQ,GAAG;KACtC,aAAa,OAAO,WAAW,SAAS;KACzC,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,OAAO,GAAG,SAAS,WAAW,GAAG,OAAO,KAAK,UAAU,GAAG,KAAK;MAC3E;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,OAAO,GAAG,UAAU,WAAW,GAAG,QAAQ,KAAK,UAAU,GAAG,MAAM;MAC7E,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;EAC1B,MAAM,YAAY,cAAc,MAAM,MAAM,EAAE,SAAS,UAAU,OAAO,EAAE,SAAS,SAAS;EAC5F,MAAM,aAAa,aAAa,OAAO,UAAU,SAAS,YAAY,UAAU,KAAK,SAAS;EAC9F,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,KAAK,UAAW,EAAE,UAAsC,UAAU;IAC5E,GAAI,EAAE,KAAK,EAAE,IAAI,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE;IACrC,EAAE;AACH,OAAI,WACF,QAAO;IAAE,SAAS,UAAU;IAAgB;IAAW;AAEzD,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,KAAK,UAAU,IAAI,UAAU;KACvC,GAAI,GAAG,KAAK,EAAE,IAAI,OAAO,GAAG,GAAG,EAAE,GAAG,EAAE;KACvC;KACD;AACF,OAAI,WACF,QAAO;IAAE,SAAS,UAAU;IAAgB;IAAW;AAEzD,UAAO,EAAE,WAAW;;AAEtB,MAAI,WACF,QAAO,EAAE,SAAS,UAAU,MAAgB;;AAKhD,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,WACE,OAAO,GAAG,cAAc,WAAW,GAAG,YAAY,KAAK,UAAU,GAAG,UAAU;KACjF;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;;;AAMpC,KAAI,OAAO,IAAI,aAAa,YAAY,UAAU,IAChD,QAAO,EAAE,SAAS,IAAI,UAAU;AAIlC,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;CAOvF,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,SAAS,MAAM,MAAM,EAAE,SAAS,OAAO;;AAG/D,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"}
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 ToolCall,\n} from \"./types.js\";\nimport { getLastMessageByRole, getTextContent } 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 { getTestId, slugifyTestId } from \"./helpers.js\";\nimport { DEFAULT_TEST_ID } from \"./constants.js\";\n\n/** Headers to strip when proxying — hop-by-hop (RFC 2616 §13.5.1) + client-set. */\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]);\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 * 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 fixture: Fixture;\n fixtures: Fixture[];\n warnings?: string[];\n logger: Logger;\n}): PersistFixtureResult {\n const { record, providerKey, testId, fixture, fixtures, warnings = [], logger } = opts;\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 if (isEmptyMatch) {\n logger.warn(\"Recorded fixture has empty match criteria — skipping in-memory registration\");\n }\n\n if (record.proxyOnly) {\n logger.info(`Proxied ${providerKey} request (proxy-only mode)`);\n return { kind: \"skipped\" };\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 `${providerKey}-${timestamp}-${crypto.randomUUID().slice(0, 8)}.json`,\n );\n } else {\n filepath = path.join(fixturePath, slug, `${providerKey}.json`);\n mergeExisting = true;\n }\n } else {\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const timestampFile = `${providerKey}-${timestamp}-${crypto.randomUUID().slice(0, 8)}.json`;\n filepath = fixture.match.context\n ? path.join(fixturePath, fixture.match.context, 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 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 let fileContent: { fixtures: unknown[]; _warning?: string };\n if (mergeExisting && fs.existsSync(filepath)) {\n try {\n const existing = JSON.parse(fs.readFileSync(filepath, \"utf-8\"));\n fileContent = { fixtures: [...(existing.fixtures ?? []), fixture] };\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 fileContent._warning = fileWarnings.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 const tmpPath = filepath + \".tmp.\" + process.pid;\n fs.writeFileSync(tmpPath, JSON.stringify(fileContent, null, 2), \"utf-8\");\n fs.renameSync(tmpPath, filepath);\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 }\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: Record<string, string> = {};\n for (const [name, val] of Object.entries(req.headers)) {\n if (val !== undefined && !STRIP_HEADERS.has(name)) {\n forwardHeaders[name] = Array.isArray(val) ? val.join(\", \") : val;\n }\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\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 frameTimestamps: number[] = [];\n let streamStartTime = 0;\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 );\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 frameTimestamps = result.frameTimestamps;\n streamStartTime = result.streamStartTime;\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 // SSE headers already sent — gracefully close the connection\n res.end();\n }\n return \"relayed\";\n }\n\n // Detect streaming response and collapse if necessary.\n // NOTE: collapse buffers the entire upstream body in memory. Fine for\n // current chat-completions traffic (responses are small), but revisit if\n // this path ever proxies long-lived or large streams — both the buffer\n // here and the hook below receive the full payload.\n const contentType = upstreamHeaders[\"content-type\"];\n const ctString = Array.isArray(contentType) ? contentType.join(\", \") : (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\n const isAudioResponse = ctString.toLowerCase().startsWith(\"audio/\");\n if (isAudioResponse && rawBuffer.length > 0) {\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 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 // 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.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 };\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 fixtureResponse = {\n content: collapsed.content,\n toolCalls: sanitizedToolCalls,\n ...reasoningSpread,\n ...reasoningSignatureSpread,\n ...redactedThinkingSpread,\n ...webSearchesSpread,\n };\n } else {\n fixtureResponse = {\n toolCalls: sanitizedToolCalls,\n ...reasoningSpread,\n ...reasoningSignatureSpread,\n ...redactedThinkingSpread,\n ...webSearchesSpread,\n };\n }\n } else {\n fixtureResponse = {\n content: collapsed.content ?? \"\",\n ...reasoningSpread,\n ...reasoningSignatureSpread,\n ...redactedThinkingSpread,\n ...webSearchesSpread,\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 returns arbitrary, model-specific JSON shapes (images, video URLs,\n // audio file objects, etc.). Round-trip the payload verbatim instead of\n // letting buildFixtureResponse mis-classify it as ImageResponse / VideoResponse.\n if (request._endpointType === \"fal\" && parsedResponse !== null) {\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 let encodingFormat: string | undefined;\n try {\n encodingFormat = rawBody ? JSON.parse(rawBody).encoding_format : undefined;\n } catch (err) {\n defaults.logger.debug(\n `Could not parse encoding_format from raw body: ${err instanceof Error ? err.message : \"unknown error\"}`,\n );\n }\n fixtureResponse = buildFixtureResponse(\n parsedResponse,\n upstreamStatus,\n encodingFormat,\n defaults.logger,\n );\n }\n }\n\n // If the client disconnected mid-stream, the collected data is likely\n // truncated. Saving a partial fixture is worse than saving none — skip\n // fixture persistence entirely.\n if (clientDisconnected) {\n defaults.logger.warn(\n \"Client disconnected mid-stream — skipping fixture save to avoid truncated data\",\n );\n return \"relayed\";\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\", 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\nfunction clampTimeout(value: number | undefined, fallback: number): number {\n if (value == null || !Number.isFinite(value) || value <= 0) return fallback;\n return 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): 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 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 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 = Array.isArray(ct) ? ct.join(\", \") : (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 const streamStartTime = 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 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 // Stop relaying if the client disconnects mid-stream.\n // Check writableFinished to distinguish normal completion (where\n // \"close\" also fires) from premature client disconnects.\n clientRes.on(\"close\", () => {\n if (!clientRes.writableFinished) {\n clientDisconnected = true;\n req.destroy();\n }\n });\n }\n const chunks: Buffer[] = [];\n res.on(\"data\", (chunk: Buffer) => {\n chunks.push(chunk);\n\n // Capture per-frame timestamps for SSE/NDJSON streams.\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 (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).\n for (let fi = 0; fi < parts.length - 1; fi++) {\n if (parts[fi].trim().length > 0) {\n frameTimestamps.push(Date.now());\n }\n }\n // Last part stays in buffer (may be incomplete)\n frameBuffer = parts[parts.length - 1];\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 if (isBinaryEventStream) {\n binaryFrameBuffer = Buffer.concat([binaryFrameBuffer, chunk]);\n while (binaryFrameBuffer.length >= 4) {\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 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(\"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.\n if (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 resolve({\n status: res.statusCode ?? 500,\n headers: res.headers,\n body: rawBuffer.toString(),\n rawBuffer,\n streamedToClient,\n clientDisconnected,\n frameTimestamps,\n streamStartTime,\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 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 * 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 encodingFormat?: string,\n logger?: Logger,\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 if (typeof first.embedding === \"string\" && encodingFormat === \"base64\") {\n const buf = Buffer.from(first.embedding, \"base64\");\n if (buf.byteLength % 4 !== 0) {\n // Malformed embedding — return a zero-dimension embedding fixture\n return { embedding: [] };\n }\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 // OpenAI image generation: { created, data: [{ url, b64_json, revised_prompt }] }\n if (first.url || first.b64_json) {\n const images = (obj.data as Array<Record<string, unknown>>).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 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: \"...\", ... }\n if (\n typeof obj.text === \"string\" &&\n (obj.task === \"transcribe\" || obj.language !== undefined || obj.duration !== undefined)\n ) {\n return {\n transcription: {\n text: obj.text as string,\n ...(obj.language ? { language: String(obj.language) } : {}),\n ...(obj.duration !== undefined ? { duration: Number(obj.duration) } : {}),\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: typeof o.arguments === \"string\" ? o.arguments : JSON.stringify(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 const openaiReasoning =\n typeof message.reasoning_content === \"string\" && message.reasoning_content.length > 0\n ? message.reasoning_content\n : undefined;\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: String(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 };\n }\n return { toolCalls, ...(openaiReasoning ? { reasoning: openaiReasoning } : {}) };\n }\n // Text content only\n if (hasContent) {\n return {\n content: message.content as string,\n ...(openaiReasoning ? { reasoning: openaiReasoning } : {}),\n };\n }\n // Recognized OpenAI shape but empty content (e.g. content filtering, zero max_tokens)\n return { content: \"\", ...(openaiReasoning ? { reasoning: openaiReasoning } : {}) };\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: typeof b.input === \"string\" ? b.input : JSON.stringify(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\n const audioParts = parts.filter(\n (p: Record<string, unknown>) =>\n p.inlineData &&\n typeof (p.inlineData as Record<string, unknown>).mimeType === \"string\" &&\n ((p.inlineData as Record<string, unknown>).mimeType as string).startsWith(\"audio/\"),\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: String(inlineData.mimeType),\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: typeof fc.args === \"string\" ? fc.args : JSON.stringify(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: typeof tu.input === \"string\" ? tu.input : JSON.stringify(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 const textBlock = contentBlocks.find((b) => b.type === \"text\" && typeof b.text === \"string\");\n const hasContent = textBlock && typeof textBlock.text === \"string\" && textBlock.text.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 : JSON.stringify((b.function as Record<string, unknown>)?.arguments),\n ...(b.id ? { id: String(b.id) } : {}),\n }));\n if (hasContent) {\n return { content: textBlock.text as string, 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 : JSON.stringify(fn?.arguments),\n ...(tc.id ? { id: String(tc.id) } : {}),\n };\n });\n if (hasContent) {\n return { content: textBlock.text as string, toolCalls };\n }\n return { toolCalls };\n }\n if (hasContent) {\n return { content: textBlock.text as string };\n }\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:\n typeof fn.arguments === \"string\" ? fn.arguments : JSON.stringify(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 if (typeof obj.response === \"string\" && \"done\" in obj) {\n return { content: obj.response };\n }\n\n // Fallback: unknown format — save as error\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 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 = messages.some((m) => m.role === \"tool\");\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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"}