@copilotkit/aimock 1.29.0 → 1.31.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 (207) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +34 -0
  4. package/README.md +1 -1
  5. package/dist/agui-types.d.ts.map +1 -1
  6. package/dist/bedrock-converse.cjs +63 -31
  7. package/dist/bedrock-converse.cjs.map +1 -1
  8. package/dist/bedrock-converse.d.cts.map +1 -1
  9. package/dist/bedrock-converse.d.ts.map +1 -1
  10. package/dist/bedrock-converse.js +65 -33
  11. package/dist/bedrock-converse.js.map +1 -1
  12. package/dist/bedrock.cjs +95 -33
  13. package/dist/bedrock.cjs.map +1 -1
  14. package/dist/bedrock.d.cts.map +1 -1
  15. package/dist/bedrock.d.ts.map +1 -1
  16. package/dist/bedrock.js +97 -35
  17. package/dist/bedrock.js.map +1 -1
  18. package/dist/cli.cjs +18 -1
  19. package/dist/cli.cjs.map +1 -1
  20. package/dist/cli.js +18 -1
  21. package/dist/cli.js.map +1 -1
  22. package/dist/cohere.cjs +49 -15
  23. package/dist/cohere.cjs.map +1 -1
  24. package/dist/cohere.d.cts.map +1 -1
  25. package/dist/cohere.d.ts.map +1 -1
  26. package/dist/cohere.js +51 -17
  27. package/dist/cohere.js.map +1 -1
  28. package/dist/config-loader.d.ts.map +1 -1
  29. package/dist/elevenlabs-audio.cjs +8 -4
  30. package/dist/elevenlabs-audio.cjs.map +1 -1
  31. package/dist/elevenlabs-audio.d.cts.map +1 -1
  32. package/dist/elevenlabs-audio.d.ts.map +1 -1
  33. package/dist/elevenlabs-audio.js +10 -6
  34. package/dist/elevenlabs-audio.js.map +1 -1
  35. package/dist/embeddings.cjs +4 -3
  36. package/dist/embeddings.cjs.map +1 -1
  37. package/dist/embeddings.d.cts.map +1 -1
  38. package/dist/embeddings.d.ts.map +1 -1
  39. package/dist/embeddings.js +6 -5
  40. package/dist/embeddings.js.map +1 -1
  41. package/dist/fal-audio.cjs +18 -8
  42. package/dist/fal-audio.cjs.map +1 -1
  43. package/dist/fal-audio.d.cts.map +1 -1
  44. package/dist/fal-audio.d.ts.map +1 -1
  45. package/dist/fal-audio.js +22 -12
  46. package/dist/fal-audio.js.map +1 -1
  47. package/dist/fal.cjs +55 -46
  48. package/dist/fal.cjs.map +1 -1
  49. package/dist/fal.d.cts +8 -0
  50. package/dist/fal.d.cts.map +1 -1
  51. package/dist/fal.d.ts +8 -0
  52. package/dist/fal.d.ts.map +1 -1
  53. package/dist/fal.js +58 -49
  54. package/dist/fal.js.map +1 -1
  55. package/dist/gemini-embeddings.cjs +4 -3
  56. package/dist/gemini-embeddings.cjs.map +1 -1
  57. package/dist/gemini-embeddings.js +6 -5
  58. package/dist/gemini-embeddings.js.map +1 -1
  59. package/dist/gemini-interactions.cjs +3 -3
  60. package/dist/gemini-interactions.cjs.map +1 -1
  61. package/dist/gemini-interactions.d.cts.map +1 -1
  62. package/dist/gemini-interactions.d.ts.map +1 -1
  63. package/dist/gemini-interactions.js +5 -5
  64. package/dist/gemini-interactions.js.map +1 -1
  65. package/dist/gemini.cjs +55 -24
  66. package/dist/gemini.cjs.map +1 -1
  67. package/dist/gemini.d.cts.map +1 -1
  68. package/dist/gemini.d.ts.map +1 -1
  69. package/dist/gemini.js +57 -26
  70. package/dist/gemini.js.map +1 -1
  71. package/dist/helpers.cjs +120 -2
  72. package/dist/helpers.cjs.map +1 -1
  73. package/dist/helpers.d.cts +43 -3
  74. package/dist/helpers.d.cts.map +1 -1
  75. package/dist/helpers.d.ts +43 -3
  76. package/dist/helpers.d.ts.map +1 -1
  77. package/dist/helpers.js +117 -3
  78. package/dist/helpers.js.map +1 -1
  79. package/dist/images.cjs +12 -6
  80. package/dist/images.cjs.map +1 -1
  81. package/dist/images.d.cts.map +1 -1
  82. package/dist/images.d.ts.map +1 -1
  83. package/dist/images.js +14 -8
  84. package/dist/images.js.map +1 -1
  85. package/dist/index.cjs +13 -2
  86. package/dist/index.d.cts +4 -3
  87. package/dist/index.d.ts +4 -3
  88. package/dist/index.js +6 -5
  89. package/dist/llmock.cjs +2 -1
  90. package/dist/llmock.cjs.map +1 -1
  91. package/dist/llmock.d.cts.map +1 -1
  92. package/dist/llmock.d.ts.map +1 -1
  93. package/dist/llmock.js +2 -1
  94. package/dist/llmock.js.map +1 -1
  95. package/dist/messages.cjs +325 -85
  96. package/dist/messages.cjs.map +1 -1
  97. package/dist/messages.d.cts.map +1 -1
  98. package/dist/messages.d.ts.map +1 -1
  99. package/dist/messages.js +327 -87
  100. package/dist/messages.js.map +1 -1
  101. package/dist/metrics.cjs +9 -0
  102. package/dist/metrics.cjs.map +1 -1
  103. package/dist/metrics.d.cts.map +1 -1
  104. package/dist/metrics.d.ts.map +1 -1
  105. package/dist/metrics.js +7 -1
  106. package/dist/metrics.js.map +1 -1
  107. package/dist/model-utils.cjs +68 -0
  108. package/dist/model-utils.cjs.map +1 -1
  109. package/dist/model-utils.js +68 -1
  110. package/dist/model-utils.js.map +1 -1
  111. package/dist/ollama.cjs +58 -21
  112. package/dist/ollama.cjs.map +1 -1
  113. package/dist/ollama.d.cts.map +1 -1
  114. package/dist/ollama.d.ts.map +1 -1
  115. package/dist/ollama.js +60 -23
  116. package/dist/ollama.js.map +1 -1
  117. package/dist/openrouter-video.cjs +1609 -0
  118. package/dist/openrouter-video.cjs.map +1 -0
  119. package/dist/openrouter-video.d.cts +154 -0
  120. package/dist/openrouter-video.d.cts.map +1 -0
  121. package/dist/openrouter-video.d.ts +154 -0
  122. package/dist/openrouter-video.d.ts.map +1 -0
  123. package/dist/openrouter-video.js +1601 -0
  124. package/dist/openrouter-video.js.map +1 -0
  125. package/dist/recorder.cjs +97 -15
  126. package/dist/recorder.cjs.map +1 -1
  127. package/dist/recorder.d.cts.map +1 -1
  128. package/dist/recorder.d.ts.map +1 -1
  129. package/dist/recorder.js +96 -17
  130. package/dist/recorder.js.map +1 -1
  131. package/dist/responses.cjs +26 -12
  132. package/dist/responses.cjs.map +1 -1
  133. package/dist/responses.d.cts +1 -1
  134. package/dist/responses.d.cts.map +1 -1
  135. package/dist/responses.d.ts +1 -1
  136. package/dist/responses.d.ts.map +1 -1
  137. package/dist/responses.js +28 -14
  138. package/dist/responses.js.map +1 -1
  139. package/dist/router.cjs +37 -8
  140. package/dist/router.cjs.map +1 -1
  141. package/dist/router.d.cts +30 -1
  142. package/dist/router.d.cts.map +1 -1
  143. package/dist/router.d.ts +30 -1
  144. package/dist/router.d.ts.map +1 -1
  145. package/dist/router.js +37 -9
  146. package/dist/router.js.map +1 -1
  147. package/dist/server.cjs +173 -19
  148. package/dist/server.cjs.map +1 -1
  149. package/dist/server.d.cts +2 -0
  150. package/dist/server.d.cts.map +1 -1
  151. package/dist/server.d.ts +2 -0
  152. package/dist/server.d.ts.map +1 -1
  153. package/dist/server.js +176 -22
  154. package/dist/server.js.map +1 -1
  155. package/dist/speech.cjs +4 -2
  156. package/dist/speech.cjs.map +1 -1
  157. package/dist/speech.d.cts.map +1 -1
  158. package/dist/speech.d.ts.map +1 -1
  159. package/dist/speech.js +6 -4
  160. package/dist/speech.js.map +1 -1
  161. package/dist/stream-collapse.cjs +44 -1
  162. package/dist/stream-collapse.cjs.map +1 -1
  163. package/dist/stream-collapse.d.cts +28 -0
  164. package/dist/stream-collapse.d.cts.map +1 -1
  165. package/dist/stream-collapse.d.ts +28 -0
  166. package/dist/stream-collapse.d.ts.map +1 -1
  167. package/dist/stream-collapse.js +44 -2
  168. package/dist/stream-collapse.js.map +1 -1
  169. package/dist/transcription.cjs +4 -2
  170. package/dist/transcription.cjs.map +1 -1
  171. package/dist/transcription.d.cts.map +1 -1
  172. package/dist/transcription.d.ts.map +1 -1
  173. package/dist/transcription.js +6 -4
  174. package/dist/transcription.js.map +1 -1
  175. package/dist/types.d.cts +123 -8
  176. package/dist/types.d.cts.map +1 -1
  177. package/dist/types.d.ts +123 -8
  178. package/dist/types.d.ts.map +1 -1
  179. package/dist/vector-types.d.cts.map +1 -1
  180. package/dist/vector-types.d.ts.map +1 -1
  181. package/dist/video.cjs +4 -2
  182. package/dist/video.cjs.map +1 -1
  183. package/dist/video.d.cts.map +1 -1
  184. package/dist/video.d.ts.map +1 -1
  185. package/dist/video.js +6 -4
  186. package/dist/video.js.map +1 -1
  187. package/dist/ws-gemini-live.cjs +4 -3
  188. package/dist/ws-gemini-live.cjs.map +1 -1
  189. package/dist/ws-gemini-live.d.cts +2 -2
  190. package/dist/ws-gemini-live.d.cts.map +1 -1
  191. package/dist/ws-gemini-live.d.ts.map +1 -1
  192. package/dist/ws-gemini-live.js +6 -5
  193. package/dist/ws-gemini-live.js.map +1 -1
  194. package/dist/ws-realtime.cjs +4 -3
  195. package/dist/ws-realtime.cjs.map +1 -1
  196. package/dist/ws-realtime.d.cts +2 -2
  197. package/dist/ws-realtime.d.cts.map +1 -1
  198. package/dist/ws-realtime.d.ts.map +1 -1
  199. package/dist/ws-realtime.js +6 -5
  200. package/dist/ws-realtime.js.map +1 -1
  201. package/dist/ws-responses.cjs +8 -6
  202. package/dist/ws-responses.cjs.map +1 -1
  203. package/dist/ws-responses.d.cts.map +1 -1
  204. package/dist/ws-responses.d.ts.map +1 -1
  205. package/dist/ws-responses.js +10 -8
  206. package/dist/ws-responses.js.map +1 -1
  207. package/package.json +2 -2
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openrouter-video.js","names":[],"sources":["../src/openrouter-video.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport crypto from \"node:crypto\";\nimport type {\n ChatCompletionRequest,\n Fixture,\n HandlerDefaults,\n RecordConfig,\n VideoResponse,\n} from \"./types.js\";\nimport type { Logger } from \"./logger.js\";\nimport {\n isVideoResponse,\n isErrorResponse,\n serializeErrorResponse,\n flattenHeaders,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n getContext,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { DEFAULT_TEST_ID } from \"./constants.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaos } from \"./chaos.js\";\nimport { resolveProgression } from \"./fal.js\";\nimport {\n buildFixtureMatch,\n buildForwardHeaders,\n clampTimeout,\n persistFixture,\n sanitizeHeaderValue,\n} from \"./recorder.js\";\nimport { resolveUpstreamUrl } from \"./url.js\";\n\n/**\n * OpenRouter async video lifecycle mock (`/api/v1/videos`). Mirrors the\n * dedicated OpenRouter video-generation API: submit returns a job envelope,\n * status polls advance `pending → in_progress → completed | failed`, and a\n * `/content` endpoint serves the bytes. With `record.providers.openrouter`\n * configured, unmatched submits become a live interactive proxy: the submit\n * is forwarded upstream and answered with a mock-rewritten envelope, each\n * client poll is proxied upstream 1:1, and a completed job is captured\n * eagerly as a fixture (strict mode still wins over record).\n */\n\ninterface OpenRouterVideoRequest {\n model?: string;\n prompt?: string;\n [key: string]: unknown;\n}\n\nconst DEFAULT_OPENROUTER_VIDEO_MODEL = \"bytedance/seedance-2.0\";\n\n// ─── OpenRouterVideoJobMap (TTL + bounded) ──────────────────────────────────\n\nexport const OPENROUTER_VIDEO_MAX_ENTRIES = 10_000;\nconst OPENROUTER_VIDEO_TTL_MS = 3_600_000; // 1 hour\n\ntype OpenRouterVideoStatus = \"pending\" | \"in_progress\" | \"completed\" | \"failed\";\n\ninterface OpenRouterVideoReplayJob {\n kind: \"replay\";\n jobId: string;\n status: OpenRouterVideoStatus;\n /** Number of status polls the caller has made against this job. */\n pollCount: number;\n /** Poll-count threshold for `pending → in_progress` transition. */\n pollsBeforeInProgress: number;\n /** Poll-count threshold for the transition to the terminal status. */\n pollsBeforeCompleted: number;\n /** The matched fixture's video object (terminal status, bytes, cost, error). */\n video: VideoResponse[\"video\"];\n /**\n * Latch for the empty-`error` authoring warn on failed polls — a polling\n * loop must surface the fixture defect once per job, not once per poll.\n */\n emptyErrorWarned?: boolean;\n /**\n * Latch for the per-download fixture-defect warns on the content endpoint\n * (b64 corruption / empty b64 / ignored url). The job's `video` is\n * immutable after submit, so the warn set is identical on every download —\n * surface it once per job, not once per re-download.\n */\n contentWarnsLatched?: boolean;\n}\n\n/**\n * A job whose lifecycle is proxied live upstream (record mode, no fixture\n * matched at submit). Every client poll is forwarded 1:1 to\n * `upstreamPollingUrl`; when the upstream reports a terminal status the\n * entry is captured as a fixture and MUTATED into a terminal\n * OpenRouterVideoReplayJob — so the content endpoint serves it like any\n * replay job. Under `record.proxyOnly` nothing is ever captured or mutated:\n * the job stays `kind: \"record\"` (terminal statuses included) and the\n * content endpoint live-proxies the stored upstream `unsigned_urls`.\n */\ninterface OpenRouterVideoRecordJob {\n kind: \"record\";\n /** The mock-issued jobId the client polls with. */\n jobId: string;\n /**\n * Last upstream status relayed to the client. Terminal values occur under\n * proxyOnly (the job stays a live proxy forever) and during the capturing\n * window — `completed` is set synchronously with `capturing` so the content\n * endpoint can live-proxy downloads while the eager capture is in flight.\n * Outside those two cases the capturing path mutates the entry into a\n * replay job at the terminal poll.\n */\n status: OpenRouterVideoStatus;\n /** Upstream's own job id (recorded as the fixture's video.id). */\n upstreamJobId: string;\n /** Origin-validated absolute upstream URL proxied on every client poll. */\n upstreamPollingUrl: string;\n /**\n * Match snapshot built at submit time (post requestTransform). Its `model`\n * is the submitted model as recorded by the standard model-normalization\n * rules — date suffixes stripped unless `recordFullModelVersion`; model-less\n * submits record the assumed default model.\n */\n match: Fixture[\"match\"];\n /**\n * Upstream content URLs from a completed poll, stored under proxyOnly and\n * during the capturing window so the content endpoint can live-proxy the\n * bytes (never cached). Stored UNFILTERED — positions must line up with the\n * indexes the rewritten relay handed the client — so entries may be\n * non-strings; the content proxy skips unusable entries at use time.\n */\n upstreamUnsignedUrls?: unknown[];\n /**\n * Set synchronously before the first await of the eager-capture sequence so\n * a concurrent completed poll relays the upstream body instead of starting\n * a second capture/persist.\n */\n capturing?: boolean;\n /**\n * Latches for the per-poll upstream-defect warns in rewriteRecordPollBody\n * (mirrors emptyErrorWarned): a polling loop must surface each defect once\n * per job, not once per poll.\n */\n nonArrayUrlsWarned?: boolean;\n badCostWarned?: boolean;\n}\n\nexport type OpenRouterVideoJob = OpenRouterVideoReplayJob | OpenRouterVideoRecordJob;\n\ninterface OpenRouterVideoEntry {\n job: OpenRouterVideoJob;\n createdAt: number;\n}\n\n/**\n * Per-testId job state for the OpenRouter video handler. Mirrors\n * FalQueueStateMap (fal.ts): lazy TTL eviction on `get`, FIFO eviction of the\n * oldest entries on `set` when over capacity, no background sweep timer.\n * Deliberate divergences from FalQueueStateMap:\n * - lifetime: this map is per-server-instance (cleared on server\n * close/reset) while FalQueueStateMap is module-global;\n * - `set()` refreshes: delete-before-set moves a re-inserted key to the\n * back of the FIFO order AND resets its createdAt, so a refreshed entry\n * gets a fresh TTL — FalQueueStateMap's plain overwrite keeps the\n * original insertion slot and is never used as a TTL refresh.\n * Keys are `${testId}:${jobId}`.\n */\nexport class OpenRouterVideoJobMap {\n private readonly entries = new Map<string, OpenRouterVideoEntry>();\n private worldGeneration = 0;\n\n /**\n * Monotonic world-generation counter, incremented by `clear()` (a fixtures\n * reset). Identity checks (`jobs.get(key) === job`) can only guard\n * continuations whose job was ALREADY inserted — a continuation that has\n * not inserted yet (the submit proxy's upstream fetch) captures this value\n * before its await and compares after, so it can detect that the world it\n * belongs to was reset mid-flight and skip seeding the new world.\n */\n get generation(): number {\n return this.worldGeneration;\n }\n\n get(key: string): OpenRouterVideoJob | undefined {\n const entry = this.entries.get(key);\n if (!entry) return undefined;\n if (Date.now() - entry.createdAt > OPENROUTER_VIDEO_TTL_MS) {\n this.entries.delete(key);\n return undefined;\n }\n return entry.job;\n }\n\n set(key: string, job: OpenRouterVideoJob): void {\n // Delete-before-set so a refreshed key moves to the BACK of the Map's\n // insertion order. A plain overwrite keeps the original insertion slot,\n // so the FIFO eviction below would treat a freshly TTL-refreshed entry\n // as the oldest and could evict it under capacity pressure — breaking\n // the documented \"each successful proxied poll refreshes the TTL\"\n // guarantee.\n this.entries.delete(key);\n this.entries.set(key, { job, createdAt: Date.now() });\n if (this.entries.size > OPENROUTER_VIDEO_MAX_ENTRIES) {\n const excess = this.entries.size - OPENROUTER_VIDEO_MAX_ENTRIES;\n const iter = this.entries.keys();\n for (let i = 0; i < excess; i++) {\n const next = iter.next();\n if (!next.done) this.entries.delete(next.value);\n }\n }\n }\n\n delete(key: string): boolean {\n return this.entries.delete(key);\n }\n\n clear(): void {\n this.entries.clear();\n this.worldGeneration++;\n }\n\n get size(): number {\n return this.entries.size;\n }\n}\n\n// ─── Job progression ────────────────────────────────────────────────────────\n\n/**\n * Maps the fixture's terminal video status onto the job lifecycle. Anything\n * that is not \"failed\" — including a fixture authored as \"processing\" — is\n * treated as completed, since this surface always drives jobs to a terminal\n * state. handleOpenRouterVideoCreate warns when a \"processing\" fixture is\n * coerced this way.\n */\nfunction terminalStatus(job: OpenRouterVideoReplayJob): OpenRouterVideoStatus {\n return job.video.status === \"failed\" ? \"failed\" : \"completed\";\n}\n\n/**\n * Mutates a job in place to advance its state on a status poll.\n * `pending → in_progress → completed | failed` based on poll-count thresholds.\n * No-op once terminal. The in_progress threshold is checked first so a job\n * whose thresholds are equal still spends one poll in in_progress instead of\n * jumping straight to the terminal status (fal advanceJob semantics).\n */\nfunction advanceJob(job: OpenRouterVideoReplayJob): void {\n if (job.status === \"completed\" || job.status === \"failed\") return;\n\n job.pollCount += 1;\n if (job.status === \"pending\" && job.pollCount >= job.pollsBeforeInProgress) {\n job.status = \"in_progress\";\n } else if (job.pollCount >= job.pollsBeforeCompleted) {\n job.status = terminalStatus(job);\n }\n}\n\n/**\n * First non-empty value of a possibly array-typed, possibly comma-joined\n * header. An empty header value or a leading-comma list (\", host\") would\n * otherwise yield \"\" — triggering a spurious rejection warn and discarding\n * valid later entries — so empty segments are skipped.\n */\nfunction firstForwardedValue(header: string | string[] | undefined): string | undefined {\n const raw = Array.isArray(header) ? header.join(\",\") : header;\n if (raw === undefined) return undefined;\n for (const segment of raw.split(\",\")) {\n const trimmed = segment.trim();\n if (trimmed) return trimmed;\n }\n return undefined;\n}\n\n// Conservative host[:port] shape for x-forwarded-host. Spaces, slashes,\n// userinfo, or any other URL-structure character would corrupt (or smuggle\n// paths into) the generated URLs the value is interpolated into. Underscores\n// are admitted: the value feeds URL-string interpolation, not DNS validation,\n// and underscore hostnames are routine in docker-compose/k8s networks\n// (e.g. my_project_aimock:4010).\nconst FORWARDED_HOST_RE = /^[a-zA-Z0-9._-]+(:\\d+)?$/;\n// Bracketed IPv6 literal host[:port], e.g. [::1] or [::1]:8080 — the bare\n// RE above cannot admit \":\" inside the host without also admitting junk.\nconst FORWARDED_HOST_IPV6_RE = /^\\[[0-9a-fA-F:.]+\\](:\\d+)?$/;\n\nfunction requestBase(req: http.IncomingMessage, logger: Logger): string {\n // Honor x-forwarded-proto and x-forwarded-host so generated URLs survive a\n // TLS-terminating or host-rewriting proxy in front of the mock. First\n // non-empty value wins on comma-joined lists.\n const candidate = firstForwardedValue(req.headers[\"x-forwarded-proto\"])?.toLowerCase();\n // Allowlist http/https — any other value (ws, junk header data) falls back.\n const proto = candidate === \"http\" || candidate === \"https\" ? candidate : \"http\";\n // Like the proto allowlist, a forwarded host that doesn't look like a bare\n // host[:port] (or a bracketed IPv6 literal) falls back to the Host header —\n // with a warn, so a misconfigured proxy isn't silently ignored.\n const fwdHost = firstForwardedValue(req.headers[\"x-forwarded-host\"]);\n // The Host fallback gets the same host[:port] validation — a junk Host\n // (e.g. \"evil.com/path\") could otherwise smuggle URL structure into the\n // generated URLs. No warn: a missing/odd Host is transport-level noise,\n // unlike a misconfigured proxy's x-forwarded-host.\n const rawHost = req.headers.host;\n let host =\n rawHost !== undefined &&\n (FORWARDED_HOST_RE.test(rawHost) || FORWARDED_HOST_IPV6_RE.test(rawHost))\n ? rawHost\n : \"localhost\";\n if (fwdHost !== undefined) {\n if (FORWARDED_HOST_RE.test(fwdHost) || FORWARDED_HOST_IPV6_RE.test(fwdHost)) {\n host = fwdHost;\n } else {\n logger.warn(\n `x-forwarded-host value rejected, falling back to Host header: ${JSON.stringify(fwdHost.slice(0, 100))}`,\n );\n }\n }\n return `${proto}://${host}`;\n}\n\n/**\n * Query-string suffix embedding the request's testId into generated URLs\n * (polling_url, unsigned_urls). The @openrouter/sdk fetches these URLs with\n * standard Authorization but no aimock-specific headers (no x-test-id) — so\n * the testId must travel in the URL for getTestId's `?testId=` fallback to\n * resolve the right job scope. The default testId is omitted to keep\n * single-tenant URLs clean.\n */\nfunction testIdSuffix(testId: string, sep: \"?\" | \"&\"): string {\n return testId === DEFAULT_TEST_ID ? \"\" : `${sep}testId=${encodeURIComponent(testId)}`;\n}\n\n/** Default upstream timeout for the live lifecycle proxy (matches recorder.ts). */\nconst DEFAULT_UPSTREAM_TIMEOUT_MS = 30_000;\n\n/**\n * Abort signal for the SMALL-JSON upstream fetches on this surface (submit,\n * status poll, models listing), honoring `record.upstreamTimeoutMs` with the\n * same clamp conventions as recorder.ts (non-finite / non-positive values\n * fall back to the 30s default). Note the nuance (documented on\n * RecordConfig.upstreamTimeoutMs): these envelope-sized fetches use the value\n * as a TOTAL deadline via AbortSignal.timeout — indistinguishable from a\n * socket-idle timeout for small bodies. The byte-bearing content fetches use\n * `fetchHeadersWithTimeout` + `readBodyIdle` instead, which implement true\n * idle semantics. An abort rejects the fetch and surfaces through the\n * caller's existing failure path (502 proxy_error on submit/poll, models\n * synthesis fallback).\n */\nfunction upstreamTimeoutSignal(record: RecordConfig | undefined): AbortSignal {\n return AbortSignal.timeout(clampTimeout(record?.upstreamTimeoutMs, DEFAULT_UPSTREAM_TIMEOUT_MS));\n}\n\n/**\n * Fetch a byte-bearing upstream URL with `record.upstreamTimeoutMs` gating\n * only the HEADERS: the abort timer is cleared the moment the response head\n * arrives, so a long — but steadily progressing — body download is never\n * killed by a total deadline. Body progress is governed separately by\n * `readBodyIdle` (between-chunk idle semantics).\n */\nasync function fetchHeadersWithTimeout(\n url: URL,\n headers: Record<string, string>,\n record: RecordConfig | undefined,\n): Promise<Response> {\n const timeoutMs = clampTimeout(record?.upstreamTimeoutMs, DEFAULT_UPSTREAM_TIMEOUT_MS);\n const controller = new AbortController();\n const timer = setTimeout(\n () => controller.abort(new Error(`Upstream response headers timed out after ${timeoutMs}ms`)),\n timeoutMs,\n );\n try {\n return await fetch(url, { headers, signal: controller.signal });\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Buffer a fetch Response body with IDLE-based timeout semantics: a timer\n * clamped from `record.bodyTimeoutMs` (30s default — same clamp as\n * recorder.ts) is re-armed for every chunk, so a steadily-dripping body of\n * any total duration completes and only a silent mid-body stall rejects.\n * When `cap` > 0 the byte count is enforced DURING the read: on exceed the\n * stream is cancelled and `{ overCap: true }` is returned with NOTHING\n * oversized retained in memory — the cap is a memory guard as much as a\n * disk guard.\n */\nasync function readBodyIdle(\n res: Response,\n record: RecordConfig | undefined,\n cap = 0,\n): Promise<{ overCap: false; buf: Buffer } | { overCap: true; bytesRead: number }> {\n const body = res.body;\n if (!body) return { overCap: false, buf: Buffer.alloc(0) };\n const idleMs = clampTimeout(record?.bodyTimeoutMs, DEFAULT_UPSTREAM_TIMEOUT_MS);\n const reader = body.getReader();\n const chunks: Buffer[] = [];\n let total = 0;\n try {\n for (;;) {\n let idleTimer: NodeJS.Timeout | undefined;\n // A stream error landing AFTER the idle timeout has already won the\n // race must never become an unhandledRejection (process crash) — attach\n // a no-op rejection handler to the read promise BEFORE racing.\n // Promise.race subscribes to its inputs too, so this is deliberate\n // defense-in-depth pinning the invariant against a refactor that races\n // the read differently; the race below still observes the rejection\n // normally when the read loses first.\n const readPromise = reader.read();\n readPromise.catch(() => {});\n const result = await Promise.race([\n readPromise,\n new Promise<never>((_, reject) => {\n idleTimer = setTimeout(\n () => reject(new Error(`Upstream response body idle for ${idleMs}ms`)),\n idleMs,\n );\n }),\n ]).finally(() => clearTimeout(idleTimer));\n if (result.done) break;\n total += result.value.byteLength;\n if (cap > 0 && total > cap) {\n return { overCap: true, bytesRead: total };\n }\n chunks.push(Buffer.from(result.value));\n }\n } finally {\n // Idle expiry and the over-cap early return leave the stream open —\n // release it. After a normal completion this is a no-op.\n void reader.cancel().catch(() => {});\n }\n return { overCap: false, buf: Buffer.concat(chunks) };\n}\n\n/**\n * Cap on an upstream ERROR body (401/403 relays) buffered by the content\n * proxy. Error bodies are envelope-sized JSON in practice — 64 KB is generous\n * — and unlike the video bytes they are buffered in full before the relay, so\n * an unbounded read would be a memory hole on a hostile upstream.\n */\nconst OPENROUTER_VIDEO_ERROR_BODY_CAP = 64 * 1024;\n\n/**\n * Cap on the small-JSON upstream envelope bodies (submit, status poll, models\n * listing) buffered before parse/relay. Envelopes are KB-scale in practice —\n * 1 MB is generous headroom even for a large models listing — and, like the\n * error-body cap above, they are buffered in full, so an unbounded `text()`\n * would be a memory hole on a hostile upstream.\n */\nconst OPENROUTER_VIDEO_ENVELOPE_BODY_CAP = 1024 * 1024;\n\n/**\n * Bounded read of a small-JSON upstream envelope body (submit, status poll,\n * models listing): readBodyIdle's idle semantics plus the envelope cap. An\n * over-cap body is a hard failure — the throw surfaces through each caller's\n * existing failure path (502 proxy_error on submit/poll, models synthesis\n * fallback) without retaining anything oversized in memory.\n */\nasync function readEnvelopeText(res: Response, record: RecordConfig | undefined): Promise<string> {\n const read = await readBodyIdle(res, record, OPENROUTER_VIDEO_ENVELOPE_BODY_CAP);\n if (read.overCap) {\n throw new Error(\n `Upstream envelope body exceeded ${OPENROUTER_VIDEO_ENVELOPE_BODY_CAP} bytes (read aborted at ${read.bytesRead})`,\n );\n }\n return read.buf.toString(\"utf8\");\n}\n\n/**\n * Rewrite an upstream poll body for relay to the client. Every field is\n * relayed verbatim EXCEPT the ones that would let the client escape the mock\n * or learn upstream identifiers:\n * - `id` → the mock jobId;\n * - a present `polling_url` → the mock's poll URL (testId embedded);\n * - a present `unsigned_urls` array → an array of the SAME length whose\n * element i is the mock content URL with `?index=i`. On post-capture\n * REPLAYS the mock content endpoint serves the index-0 bytes for every\n * index (only index 0 is captured); during the capture window and under\n * proxyOnly the index selects the live-proxied upstream URL.\n * `usage` passes through untouched: a non-number `usage.cost` warns but is\n * never coerced or dropped, and no usage is ever invented.\n */\nfunction rewriteRecordPollBody(args: {\n upstreamBody: Record<string, unknown>;\n job: OpenRouterVideoRecordJob;\n req: http.IncomingMessage;\n testId: string;\n logger: Logger;\n}): Record<string, unknown> {\n const { upstreamBody, job, req, testId, logger } = args;\n const base = requestBase(req, logger);\n const body: Record<string, unknown> = { ...upstreamBody, id: job.jobId };\n if (upstreamBody.polling_url !== undefined) {\n body.polling_url = `${base}/api/v1/videos/${job.jobId}${testIdSuffix(testId, \"?\")}`;\n }\n if (Array.isArray(upstreamBody.unsigned_urls)) {\n body.unsigned_urls = upstreamBody.unsigned_urls.map(\n (_url, i) =>\n `${base}/api/v1/videos/${job.jobId}/content?index=${i}${testIdSuffix(testId, \"&\")}`,\n );\n } else if (upstreamBody.unsigned_urls !== undefined) {\n // A non-array unsigned_urls cannot be index-rewritten — relaying it\n // verbatim would leak the upstream value to the client, the exact escape\n // this rewrite exists to prevent. Strip it with a warn (the any-value\n // treatment polling_url gets above). Latched once per job (mirroring\n // emptyErrorWarned): a polling loop must not spam the warn.\n delete body.unsigned_urls;\n if (!job.nonArrayUrlsWarned) {\n job.nonArrayUrlsWarned = true;\n logger.warn(\n `Upstream video job ${job.upstreamJobId} reported a non-array unsigned_urls (${JSON.stringify(upstreamBody.unsigned_urls).slice(0, 100)}) — stripped from the relay`,\n );\n }\n }\n const usage = upstreamBody.usage;\n if (usage !== null && typeof usage === \"object\" && !Array.isArray(usage)) {\n const cost = (usage as Record<string, unknown>).cost;\n if (cost !== undefined && typeof cost !== \"number\" && !job.badCostWarned) {\n job.badCostWarned = true;\n logger.warn(\n `Upstream video job ${job.upstreamJobId} reported a non-number usage.cost (${JSON.stringify(cost).slice(0, 50)}) — passing it through untouched`,\n );\n }\n }\n return body;\n}\n\n/**\n * Synthesizes a structurally valid journal body for field-validation 400s.\n * JournalEntry.body is typed `ChatCompletionRequest | null`, so the raw\n * parsed body cannot be journaled as-is — journal consumers may walk\n * `body.messages`. The result is ChatCompletionRequest-shaped: `model` is a\n * string (a non-string value is JSON-encoded so the field stays a string\n * without dropping what the caller sent) and `messages` is an empty array\n * (there is no validated prompt to wrap). Raw request fields (including\n * `prompt`) are preserved via the index signature only on this validation\n * path — the success path's syntheticReq is built from scratch and does not\n * carry them.\n */\nfunction validationJournalBody(videoReq: OpenRouterVideoRequest): ChatCompletionRequest {\n const rawModel = videoReq.model;\n const model =\n typeof rawModel === \"string\"\n ? rawModel\n : rawModel === undefined\n ? \"\"\n : JSON.stringify(rawModel);\n // Underscore-prefixed keys (`_endpointType`, `_context`, ...) are reserved\n // for handler-set discriminators that journal consumers treat as trusted —\n // strip them from the raw client body so a request cannot spoof them.\n const sanitized = Object.fromEntries(\n Object.entries(videoReq).filter(([key]) => !key.startsWith(\"_\")),\n );\n return { ...sanitized, model, messages: [] };\n}\n\n// ─── GET /api/v1/videos/{jobId} — status poll ───────────────────────────────\n\nexport async function handleOpenRouterVideoStatus(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n jobId: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n jobs: OpenRouterVideoJobMap,\n): Promise<void> {\n setCorsHeaders(res);\n const path = req.url ?? `/api/v1/videos/${jobId}`;\n const method = req.method ?? \"GET\";\n\n // Chaos rolls BEFORE the job lookup, so it cannot know whether the poll\n // would have been proxied (record job) or served locally — the label stays\n // \"internal\" even in record mode, and a chaos-dropped poll never reaches\n // the upstream.\n if (\n applyChaos(\n res,\n null,\n defaults.chaos,\n req.headers,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: null },\n \"internal\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n const testId = getTestId(req);\n const job = jobs.get(`${testId}:${jobId}`);\n\n if (!job) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 404, fixture: null },\n });\n writeErrorResponse(\n res,\n 404,\n JSON.stringify({ error: { message: `Video job ${jobId} not found`, code: 404 } }),\n );\n return;\n }\n\n if (job.kind === \"record\") {\n // Strict means nothing reaches an upstream — a record job's polls are\n // pure upstream proxies, so an effective-strict request is refused with\n // the strict 503 family (same gate as the models proxy; the submit-time\n // gate cannot help a job that was created under a strict-off override).\n if (resolveStrictMode(defaults.strict, req.headers)) {\n defaults.logger.error(\n `STRICT: video job ${jobId} is proxied live upstream (record mode) — refusing the upstream poll`,\n );\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\n JSON.stringify({\n error: {\n message: `Strict mode: video job ${jobId} is proxied live upstream (record mode) — nothing reaches an upstream under strict mode`,\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n await proxyOpenRouterVideoRecordPoll({\n req,\n res,\n job,\n key: `${testId}:${jobId}`,\n testId,\n fixtures,\n journal,\n defaults,\n jobs,\n method,\n path,\n });\n return;\n }\n\n // Guard BEFORE advancing or journaling (file convention): a client that\n // disconnected while the poll was queued gets neither a write nor a\n // journal entry — and consumes no progression step or TTL refresh, so the\n // next LIVE poll observes the state this one would have consumed.\n if (res.destroyed || res.writableEnded) return;\n advanceJob(job);\n // Refresh the TTL on every replay poll, mirroring the record-job proxy\n // path: the delete-before-set semantics also move the entry to the back of\n // the FIFO eviction order, so an actively-polled job survives both the TTL\n // and capacity pressure however long the client keeps polling.\n jobs.set(`${testId}:${jobId}`, job);\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 200, fixture: null },\n });\n\n const body: Record<string, unknown> = { id: job.jobId, status: job.status };\n if (job.status === \"completed\") {\n body.unsigned_urls = [\n `${requestBase(req, defaults.logger)}/api/v1/videos/${job.jobId}/content?index=0${testIdSuffix(testId, \"&\")}`,\n ];\n body.usage = { cost: job.video.cost ?? 0 };\n } else if (job.status === \"failed\") {\n if (job.video.error === \"\" && !job.emptyErrorWarned) {\n // An explicit-but-empty error is an authoring mistake — warn (mirroring\n // the empty-b64 warn) instead of serving an empty failure reason. Once\n // per job: a polling loop on a failed job must not spam the warn.\n job.emptyErrorWarned = true;\n defaults.logger.warn(\n `Video fixture for job ${job.jobId} has an empty error message — using the default`,\n );\n }\n body.error = job.video.error || \"Video generation failed\";\n }\n\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n}\n\n// ─── GET /api/v1/videos/{jobId}/content — download ──────────────────────────\n\n// Minimal valid-prefix MP4 placeholder served when a completed fixture has no\n// `b64` payload: a bare 24-byte `ftyp` box (major brand isom, minor 0x200,\n// compatible brands isom + mp42). Enough for clients that sniff the container\n// signature without requiring real video bytes in every fixture.\nconst PLACEHOLDER_MP4 = Buffer.from([\n 0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d, 0x00, 0x00, 0x02, 0x00,\n 0x69, 0x73, 0x6f, 0x6d, 0x6d, 0x70, 0x34, 0x32,\n]);\n\n// The `index` query param is accepted but ignored (jobs are single-video), and\n// fetching content deliberately does NOT advance job state — clients only\n// learn the content URL from a completed status poll (API fidelity; diverges\n// from fal's advance-on-result queue semantics). Exception: a completed\n// kind:\"record\" job — reachable under record.proxyOnly AND during the eager\n// capture window — live-proxies the stored upstream unsigned_urls instead,\n// honoring the index.\nexport async function handleOpenRouterVideoContent(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n jobId: string,\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n jobs: OpenRouterVideoJobMap,\n): Promise<void> {\n setCorsHeaders(res);\n const path = req.url ?? `/api/v1/videos/${jobId}/content`;\n const method = req.method ?? \"GET\";\n\n if (\n applyChaos(\n res,\n null,\n defaults.chaos,\n req.headers,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: null },\n \"internal\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n // The real endpoint requires Bearer auth even though the unsigned URL is\n // otherwise self-contained — the @openrouter/sdk fetches it with the key.\n // RFC 7235 auth schemes are case-insensitive; the credential must be\n // non-empty.\n const authorization = req.headers.authorization;\n if (!authorization || !/^bearer\\s+\\S/i.test(authorization)) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 401, fixture: null },\n });\n writeErrorResponse(\n res,\n 401,\n JSON.stringify({ error: { message: \"No auth credentials found\", code: 401 } }),\n );\n return;\n }\n\n const testId = getTestId(req);\n const job = jobs.get(`${testId}:${jobId}`);\n\n if (!job) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 404, fixture: null },\n });\n writeErrorResponse(\n res,\n 404,\n JSON.stringify({ error: { message: `Video job ${jobId} not found`, code: 404 } }),\n );\n return;\n }\n\n if (job.status !== \"completed\") {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: `Video job ${jobId} is not completed (status: ${job.status})`,\n code: 400,\n },\n }),\n );\n return;\n }\n\n const queryIdx = path.indexOf(\"?\");\n const indexParam =\n queryIdx === -1 ? null : new URLSearchParams(path.slice(queryIdx + 1)).get(\"index\");\n\n // A completed record job is reachable under record.proxyOnly (live proxy\n // forever) and during the capturing window (a concurrent poll relayed the\n // terminal body while the eager capture is still in flight). Live-proxy the\n // stored upstream URL in both cases (the bytes are never cached).\n if (job.kind === \"record\") {\n // Strict means nothing reaches an upstream — gate the live content proxy\n // exactly like the models proxy and the record-job poll path.\n if (resolveStrictMode(defaults.strict, req.headers)) {\n defaults.logger.error(\n `STRICT: video job ${jobId} content is proxied live upstream (record mode) — refusing the upstream fetch`,\n );\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\n JSON.stringify({\n error: {\n message: `Strict mode: video job ${jobId} is proxied live upstream (record mode) — nothing reaches an upstream under strict mode`,\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n await proxyOpenRouterVideoRecordContent({\n req,\n res,\n job,\n key: `${testId}:${jobId}`,\n indexParam,\n journal,\n defaults,\n jobs,\n method,\n path,\n });\n return;\n }\n\n // `index` is accepted but ignored (jobs are single-video) — warn when a\n // present value asks for anything other than index 0, since the caller is\n // silently getting index-0 bytes either way.\n if (indexParam !== null && Number(indexParam) !== 0) {\n defaults.logger.warn(\n `Video content request for job ${jobId} asked for index=${indexParam} — the index param is ignored (jobs are single-video); serving index 0`,\n );\n }\n\n let bytes: Buffer;\n if (job.video.b64) {\n bytes = Buffer.from(job.video.b64, \"base64\");\n // Node's base64 decoder is lenient — invalid characters are skipped and\n // the first \"=\" terminates the decode — so a corrupt payload silently\n // truncates instead of erroring. Compare the decoded byte count against\n // what the sanitized input length should yield (every 4 chars → 3 bytes,\n // floor for a partial final group) and warn on mismatch. Sanitization\n // mirrors the decoder: whitespace stripped, base64url normalized, and\n // everything from the first \"=\" on dropped (counting post-padding\n // characters as data chars would false-flag concatenated-but-valid\n // payloads like \"QQ==QQ==\", which Node decodes as just their first\n // group). A length check — rather than byte-exact re-encode equality —\n // tolerates valid non-canonical base64 whose final character carries\n // nonzero discarded trailing bits (e.g. \"QR\" re-encodes as \"QQ\") while\n // still catching skipped-character corruption. The decode is served as-is.\n const sanitized = job.video.b64\n .replace(/\\s+/g, \"\")\n .replace(/-/g, \"+\")\n .replace(/_/g, \"/\")\n .replace(/=.*$/, \"\");\n const expectedBytes = Math.floor((sanitized.length * 3) / 4);\n if (bytes.length === 0 && !job.contentWarnsLatched) {\n // Padding-only payloads (e.g. \"=\", \"====\") are truthy but sanitize to\n // \"\" and decode to 0 bytes — dodging both the empty-string warn and\n // the length-mismatch check below. Warn whenever a non-empty b64\n // decodes to nothing; the zero-byte body is still served as-is.\n defaults.logger.warn(\n `Video fixture b64 for job ${jobId} decoded to zero bytes — the fixture is not controlling the served content`,\n );\n }\n if (sanitized.length % 4 === 1) {\n // A length ≡ 1 (mod 4) is base64 the mismatch check cannot catch: the\n // floor formula agrees with Node's lenient decode whether the payload\n // is genuinely truncated or merely contains invalid characters the\n // sanitizer does not strip (e.g. \"AAAA!\" decodes fully).\n if (!job.contentWarnsLatched) {\n defaults.logger.warn(\n `Video fixture b64 for job ${jobId} has length ≡ 1 (mod 4) after sanitization (${sanitized.length} chars) — payload is malformed or contains invalid characters`,\n );\n }\n } else if (bytes.length !== expectedBytes && !job.contentWarnsLatched) {\n defaults.logger.warn(\n `Video fixture b64 for job ${jobId} decoded to ${bytes.length} bytes where its length implies ${expectedBytes} — likely corrupt base64`,\n );\n }\n } else {\n if (job.video.b64 === \"\" && !job.contentWarnsLatched) {\n // An explicit-but-empty b64 is indistinguishable from an absent one to\n // the truthiness check above — warn so the author learns the fixture\n // is not controlling the served bytes.\n defaults.logger.warn(\n `Video fixture for job ${jobId} has an empty b64 — serving the placeholder MP4`,\n );\n }\n if (job.video.url && !job.contentWarnsLatched) {\n // Every other coercion on this surface warns — so does dropping the\n // author's url. The real OpenRouter content endpoint serves bytes, not\n // a redirect, so the mock has nothing to do with a url-only fixture.\n defaults.logger.warn(\n `Video fixture for job ${jobId} sets video.url but no b64 — url is ignored on the OpenRouter content endpoint; use b64 to control the served bytes (serving the placeholder MP4)`,\n );\n }\n bytes = PLACEHOLDER_MP4;\n }\n // Latch the fixture-defect warns above once per job: video is immutable\n // after submit, so re-downloads would only repeat the identical warn set.\n job.contentWarnsLatched = true;\n\n // Guard BEFORE journaling (file convention): a client that disconnected\n // before the write gets neither a response nor a journal entry.\n if (res.destroyed || res.writableEnded) return;\n // Refresh the TTL on a successful serve, mirroring the replay poll path: a\n // client stepping through downloads (or re-fetching) keeps the job alive\n // however long the session runs. delete-before-set (jobs.set) also moves\n // the entry to the back of the FIFO eviction order.\n jobs.set(`${testId}:${jobId}`, job);\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 200, fixture: null },\n });\n\n // Always video/mp4 — the real endpoint serves video/mp4 even when the\n // client (e.g. the Speakeasy-generated @openrouter/sdk) sends\n // Accept: application/octet-stream.\n res.writeHead(200, { \"Content-Type\": \"video/mp4\", \"Content-Length\": bytes.length });\n res.end(bytes);\n}\n\n/**\n * Live-proxy a content download for a completed record job — reachable under\n * proxyOnly (forever) and during the eager-capture window: the stored\n * upstream `unsigned_urls[index || 0]` is fetched with the same same-origin\n * Bearer-forwarding gate as the eager-capture path (headers gated by\n * `upstreamTimeoutMs`, body by `bodyTimeoutMs` idle semantics), and the bytes\n * are STREAMED to the client as `video/mp4` (matching the real API regardless\n * of the upstream's content-type) — chunks are relayed as they arrive, so the\n * whole video is never buffered in memory. An upstream 401/403 passes through\n * verbatim (real-API fidelity); other pre-relay failures 502 (a mid-stream\n * stall can only abort the already-committed 200). No state is mutated and\n * nothing is cached — repeated downloads hit the upstream every time.\n * Journaled source:\"proxy\" when the relay commits. Refuses with 502 before\n * contacting the upstream when record mode was disabled mid-flight. A\n * committed relay refreshes the job's TTL (identity-guarded) — a client\n * working through a long download list must not lose the job mid-way.\n */\nasync function proxyOpenRouterVideoRecordContent(args: {\n req: http.IncomingMessage;\n res: http.ServerResponse;\n job: OpenRouterVideoRecordJob;\n key: string;\n indexParam: string | null;\n journal: Journal;\n defaults: HandlerDefaults;\n jobs: OpenRouterVideoJobMap;\n method: string;\n path: string;\n}): Promise<void> {\n const { req, res, job, key, indexParam, journal, defaults, jobs, method, path } = args;\n const logger = defaults.logger;\n\n const proxyError = (msg: string): void => {\n logger.error(`OpenRouter video content proxy failed: ${msg}`);\n // Guard BEFORE journaling (file convention): a disconnected client gets\n // neither a write nor a journal entry.\n if (res.destroyed || res.writableEnded) return;\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: {\n status: 502,\n fixture: null,\n source: \"proxy\",\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 502,\n JSON.stringify({\n error: { message: `Proxy to upstream failed: ${msg}`, type: \"proxy_error\" },\n }),\n );\n };\n\n // Recording can be disabled mid-flight (LLMock.disableRecording) — an\n // orphaned record job must fail loudly BEFORE contacting the upstream\n // (and before forwarding the client's Bearer anywhere), mirroring the\n // record-job poll gate. The snapshot below is used for every later read\n // (same discipline as the poll path): the gate and its consumers must see\n // ONE config object even if the defaults are swapped mid-relay.\n const record = defaults.record;\n if (!record) {\n proxyError(\"record mode is no longer configured for an in-flight record job\");\n return;\n }\n\n const urls = job.upstreamUnsignedUrls ?? [];\n const parsedIndex = indexParam === null ? 0 : Number(indexParam);\n const index = Number.isInteger(parsedIndex) && parsedIndex >= 0 ? parsedIndex : 0;\n if (indexParam !== null && index !== parsedIndex) {\n // Mirror the replay path's warn convention: a coerced index silently\n // changes which bytes the caller gets.\n logger.warn(\n `Video content request for job ${job.jobId} has an unusable index=${indexParam} — serving index 0`,\n );\n }\n // The stored array is UNFILTERED (positions align with the rewritten relay\n // indexes), so the addressed entry may be a non-string — skip unusable\n // entries at use time, warning before the index-0 fallback.\n const addressed = urls[index];\n let target = typeof addressed === \"string\" && addressed ? addressed : undefined;\n if (target === undefined && index !== 0) {\n // Two distinct upstream defects share this fallback — name the right one:\n // an index past the array is the CLIENT asking for more videos than the\n // upstream reported, while an in-range-but-unusable entry is the UPSTREAM\n // having reported a non-string/empty URL at that position.\n if (index >= urls.length) {\n logger.warn(\n `Video content request for job ${job.jobId} asked for index=${index} but the upstream reported only ${urls.length} unsigned_urls — serving index 0`,\n );\n } else {\n logger.warn(\n `Video content request for job ${job.jobId} asked for index=${index} but the upstream's unsigned_urls[${index}] is unusable (non-string or empty) — serving index 0`,\n );\n }\n const first = urls[0];\n target = typeof first === \"string\" && first ? first : undefined;\n }\n if (!target) {\n // Name the actual condition: an empty array is the upstream reporting no\n // URLs at all, while a non-empty array whose entry 0 is unusable is a\n // different upstream defect (the index>0 warns above draw the same line).\n proxyError(\n urls.length === 0\n ? `Upstream job ${job.upstreamJobId} completed without usable unsigned_urls`\n : `Upstream job ${job.upstreamJobId} completed but its unsigned_urls[0] is unusable (non-string or empty)`,\n );\n return;\n }\n\n let contentUrl: URL;\n try {\n contentUrl = new URL(target);\n } catch {\n proxyError(`Upstream unsigned_urls[${index}] is not a valid URL (${target.slice(0, 100)})`);\n return;\n }\n\n // The client's Bearer is forwarded ONLY to the configured provider origin —\n // the same gate as the eager-capture path.\n const providerBase = record.providers.openrouter;\n let providerOrigin: string;\n try {\n providerOrigin = new URL(providerBase ?? job.upstreamPollingUrl).origin;\n } catch {\n try {\n providerOrigin = new URL(job.upstreamPollingUrl).origin;\n } catch {\n proxyError(\n `Cannot determine the provider origin (invalid provider URL and polling URL) — refusing to forward credentials`,\n );\n return;\n }\n }\n const headers = buildForwardHeaders(req);\n if (contentUrl.origin !== providerOrigin) {\n delete headers.authorization;\n logger.warn(\n `Upstream unsigned_urls[${index}] origin ${contentUrl.origin} differs from the provider origin ${providerOrigin} — fetching content WITHOUT the client's Authorization header`,\n );\n }\n\n // Headers gated by upstreamTimeoutMs, body by bodyTimeoutMs IDLE semantics\n // — a steadily-downloading large video must never be killed by a total\n // deadline (see fetchHeadersWithTimeout).\n let contentRes: Response;\n try {\n contentRes = await fetchHeadersWithTimeout(contentUrl, headers, record);\n } catch (err) {\n proxyError(err instanceof Error ? err.message : \"Unknown proxy error\");\n return;\n }\n if (contentRes.status === 401 || contentRes.status === 403) {\n // Real-API fidelity: an upstream auth rejection is the client's\n // problem (bad or expired Bearer) — relay the upstream status and body\n // instead of wrapping them in a generic 502 proxy_error. The body read\n // is bounded like every other upstream body on this surface (the header\n // timer was cleared when the head arrived, so a bare arrayBuffer() here\n // could hang forever): idle semantics from `record.bodyTimeoutMs` plus a\n // small hard cap. Documented choices — a mid-body STALL 502s (the\n // upstream status cannot be relayed faithfully without its body), while\n // an OVER-CAP body relays the upstream status with an empty body\n // (status fidelity preserved; an error body past 64 KB is pathological).\n let errBody: Buffer;\n try {\n const read = await readBodyIdle(contentRes, record, OPENROUTER_VIDEO_ERROR_BODY_CAP);\n if (read.overCap) {\n logger.warn(\n `Upstream ${contentRes.status} error body for job ${job.upstreamJobId} exceeded ${OPENROUTER_VIDEO_ERROR_BODY_CAP} bytes — relaying the status with an empty body`,\n );\n errBody = Buffer.alloc(0);\n } else {\n errBody = read.buf;\n }\n } catch (err) {\n proxyError(err instanceof Error ? err.message : \"Unknown proxy error\");\n return;\n }\n logger.warn(\n `Upstream content host rejected the download for job ${job.upstreamJobId} (${contentRes.status}) — relaying the upstream status`,\n );\n if (res.destroyed || res.writableEnded) return;\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: {\n status: contentRes.status,\n fixture: null,\n source: \"proxy\",\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n res.writeHead(contentRes.status, {\n \"Content-Type\": contentRes.headers.get(\"content-type\") ?? \"application/json\",\n });\n res.end(errBody);\n return;\n }\n if (!contentRes.ok) {\n // Include a bounded body sample so the failure is diagnosable (every\n // other proxy error on this surface carries a body snippet). Bounded\n // exactly like the 401/403 relay above: idle semantics + the error cap.\n let sample = \"\";\n try {\n const read = await readBodyIdle(contentRes, record, OPENROUTER_VIDEO_ERROR_BODY_CAP);\n if (!read.overCap) sample = read.buf.toString(\"utf8\").slice(0, 200);\n } catch {\n // Body unreadable — the status alone still names the failure.\n }\n proxyError(`Content ${contentRes.status}${sample ? `: ${sample}` : \"\"}`);\n return;\n }\n\n // Guard BEFORE journaling: a client that disconnected mid-fetch gets\n // neither a write nor a journal entry — the journaled 200 must reflect a\n // response that actually left.\n if (res.destroyed || res.writableEnded) {\n void contentRes.body?.cancel().catch(() => {});\n return;\n }\n // Journal when the relay COMMITS (the status line is about to hit the\n // wire), then STREAM the body chunk-by-chunk: the whole video is never\n // buffered in memory and the first bytes reach the client while the\n // upstream is still sending. Journal-accuracy choice: a mid-stream abort\n // (upstream stall / client disconnect) still happened as a 200 on the wire\n // — just truncated — so journaling at commit time stays accurate to what\n // left, whereas journaling only on completion would drop truncated relays\n // entirely. Aborts are additionally logged below.\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: {\n status: 200,\n fixture: null,\n source: \"proxy\",\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n // TTL refresh at commit time, identity-guarded like every post-await map\n // touch: an actively-downloading job must not be evicted between fetches.\n if (jobs.get(key) === job) {\n jobs.set(key, job);\n }\n // Always video/mp4 — matching both the real API and the replay path above.\n // No Content-Length: the bytes are relayed as they arrive (chunked).\n res.writeHead(200, { \"Content-Type\": \"video/mp4\" });\n\n const body = contentRes.body;\n if (!body) {\n res.end();\n return;\n }\n // Same IDLE semantics as readBodyIdle: the timer is re-armed per chunk, so\n // a steadily-dripping body of any total duration completes and only a\n // silent mid-body stall aborts.\n const idleMs = clampTimeout(record.bodyTimeoutMs, DEFAULT_UPSTREAM_TIMEOUT_MS);\n const reader = body.getReader();\n try {\n for (;;) {\n let idleTimer: NodeJS.Timeout | undefined;\n // Same orphaned-read hygiene as readBodyIdle: a stream error landing\n // after the idle timeout won the race must never become an\n // unhandledRejection (see the readBodyIdle comment for the full\n // reasoning — deliberate defense-in-depth).\n const readPromise = reader.read();\n readPromise.catch(() => {});\n const result = await Promise.race([\n readPromise,\n new Promise<never>((_, reject) => {\n idleTimer = setTimeout(\n () => reject(new Error(`Upstream response body idle for ${idleMs}ms`)),\n idleMs,\n );\n }),\n ]).finally(() => clearTimeout(idleTimer));\n if (result.done) break;\n if (res.destroyed) return; // client went away mid-stream — stop relaying\n if (!res.write(Buffer.from(result.value))) {\n // Backpressure: wait for drain, bailing on close so a client that\n // disconnects mid-stall cannot wedge the relay — and BOUND the wait\n // with the same clamped bodyTimeoutMs: a stalled-OPEN client (socket\n // alive, never reading) would otherwise wedge the relay and pin the\n // upstream reader forever.\n const drained = await new Promise<boolean>((resolve) => {\n const cleanup = (): void => {\n res.off(\"drain\", onDrain);\n res.off(\"close\", onClose);\n clearTimeout(drainTimer);\n };\n const onDrain = (): void => {\n cleanup();\n resolve(true);\n };\n const onClose = (): void => {\n cleanup();\n // The loop's res.destroyed check stops the relay — though the\n // upstream reader's release can lag by up to one idle period\n // (the next read must resolve or idle-expire before the\n // destroyed check runs).\n resolve(true);\n };\n const drainTimer = setTimeout(() => {\n cleanup();\n resolve(false);\n }, idleMs);\n res.once(\"drain\", onDrain);\n res.once(\"close\", onClose);\n });\n if (!drained) {\n logger.warn(\n `OpenRouter video content relay for job ${job.upstreamJobId} aborted: the client stopped reading for ${idleMs}ms — destroying the response and releasing the upstream`,\n );\n res.destroy(); // the finally below releases the upstream reader\n return;\n }\n }\n }\n res.end();\n } catch (err) {\n // The headers are already on the wire — a 502 is no longer possible.\n // Destroy the response so the client observes a truncated transfer\n // instead of a hang, and log the abort (the journaled 200 stands: a 200\n // status line did leave — see the journal-accuracy note above).\n logger.error(\n `OpenRouter video content relay aborted mid-stream: ${err instanceof Error ? err.message : String(err)}`,\n );\n res.destroy();\n } finally {\n // Stall abort and the client-gone return leave the stream open — release\n // it. After a normal completion this is a no-op.\n void reader.cancel().catch(() => {});\n }\n}\n\n// ─── GET /api/v1/videos/models — model listing ──────────────────────────────\n\nconst DEFAULT_OPENROUTER_VIDEO_MODELS = [DEFAULT_OPENROUTER_VIDEO_MODEL, \"openai/sora-2\"];\n\nfunction modelEntry(id: string): Record<string, unknown> {\n return {\n id,\n name: id,\n supported_durations: [4, 8],\n supported_resolutions: [\"720p\", \"1080p\"],\n supported_aspect_ratios: [\"16:9\", \"9:16\", \"1:1\"],\n supported_frame_images: [],\n supported_sizes: [],\n generate_audio: false,\n seed: true,\n pricing_skus: [],\n };\n}\n\n/**\n * Synthesizes the OpenRouter video model listing from loaded fixtures —\n * video-endpoint fixtures with a string `match.model` (mirrors the Ollama\n * `/api/tags` synthesis in server.ts). Falls back to a default model set when\n * no video fixtures are loaded. Note video models do not appear in the plain\n * `/api/v1/models` listing on the real API, hence the dedicated route.\n *\n * With `record.providers.openrouter` configured the listing is proxied\n * upstream instead and relayed verbatim on success (journaled\n * source:\"proxy\", never recorded as a fixture); an upstream failure warns\n * and falls back to the synthesis below. Strict mode disables the proxy —\n * a strict request is always served the synthesized listing (strict means\n * \"nothing reaches an upstream\").\n */\nexport async function handleOpenRouterVideoModels(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n setCorsHeaders(res);\n const path = req.url ?? \"/api/v1/videos/models\";\n const method = req.method ?? \"GET\";\n\n if (\n applyChaos(\n res,\n null,\n defaults.chaos,\n req.headers,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: null },\n \"internal\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n // Snapshot (file discipline): the gate and its consumers below must see ONE\n // config object even if the defaults are swapped mid-request.\n const record = defaults.record;\n const upstreamBase = record?.providers.openrouter;\n let proxyAttemptFailed = false;\n if (upstreamBase && !resolveStrictMode(defaults.strict, req.headers)) {\n // The try covers ONLY the upstream fetch: a throw from the journal/relay\n // writes below must not be misattributed to the upstream (it would warn\n // \"proxy failed\", double-journal, and attempt a second response via the\n // synthesis fallback).\n let relay: { text: string; contentType: string } | undefined;\n try {\n const target = resolveUpstreamUrl(upstreamBase, \"/api/v1/videos/models\");\n const upstreamRes = await fetch(target, {\n headers: buildForwardHeaders(req),\n signal: upstreamTimeoutSignal(record),\n });\n const text = await readEnvelopeText(upstreamRes, record);\n if (!upstreamRes.ok) {\n throw new Error(`Models ${upstreamRes.status}: ${text.slice(0, 200)}`);\n }\n relay = {\n text,\n contentType: upstreamRes.headers.get(\"content-type\") ?? \"application/json\",\n };\n } catch (err) {\n proxyAttemptFailed = true;\n const msg = err instanceof Error ? err.message : \"Unknown proxy error\";\n defaults.logger.warn(\n `OpenRouter video models proxy failed (${msg}) — falling back to the synthesized listing`,\n );\n }\n if (relay) {\n // Guard BEFORE journaling (file convention): a client that\n // disconnected while the upstream fetch was in flight gets neither a\n // write nor a journal entry — and never the synthesis fallback.\n if (res.destroyed || res.writableEnded) return;\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n // strictOverrideField on every journaled response (the file-wide\n // convention — proxied entries included): a proxied path is reachable\n // with a strict-ON server default only via a per-request strict-OFF\n // override, which journal consumers must see.\n response: {\n status: 200,\n fixture: null,\n source: \"proxy\",\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n // Verbatim relay — listings are metadata, never recorded as fixtures.\n res.writeHead(200, { \"Content-Type\": relay.contentType });\n res.end(relay.text);\n return;\n }\n }\n\n const modelIds = new Set<string>();\n let sawVideoFixture = false;\n for (const f of fixtures) {\n if (f.match.endpoint === \"video\") {\n sawVideoFixture = true;\n if (f.match.model && typeof f.match.model === \"string\") {\n modelIds.add(f.match.model);\n }\n }\n }\n if (modelIds.size === 0 && sawVideoFixture) {\n // Video fixtures are loaded but none has a string match.model (e.g. all\n // RegExp models or onVideo registrations) — the listing silently serves\n // the default set, which can surprise fixture authors.\n defaults.logger.warn(\n \"No video fixture contributes a string model — serving the default video model set\",\n );\n }\n const ids = modelIds.size > 0 ? [...modelIds] : DEFAULT_OPENROUTER_VIDEO_MODELS;\n\n // Guard BEFORE journaling (file convention): the synthesis is reachable\n // after an AWAITED-but-failed proxy attempt above, so the client may have\n // disconnected while that fetch was in flight — it gets neither a write\n // nor a journal entry.\n if (res.destroyed || res.writableEnded) return;\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n // A synthesis serving as the fallback for a FAILED proxy attempt is\n // labeled source:\"internal\" so journal consumers can tell it apart from\n // a relay that actually came from the upstream. A synthesis whose proxy\n // was never attempted — strict-suppressed or plain no-record — omits\n // source (the surface's existing convention); a strict override that\n // suppressed the proxy is surfaced via strictOverrideField like every\n // other strict-influenced journal entry on this surface.\n response: {\n status: 200,\n fixture: null,\n ...(proxyAttemptFailed ? { source: \"internal\" as const } : {}),\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ data: ids.map((id) => modelEntry(id)) }));\n}\n\n// ─── POST /api/v1/videos — submit ───────────────────────────────────────────\n\nexport async function handleOpenRouterVideoCreate(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n jobs: OpenRouterVideoJobMap,\n): Promise<void> {\n setCorsHeaders(res);\n const path = req.url ?? \"/api/v1/videos\";\n const method = req.method ?? \"POST\";\n\n let videoReq: OpenRouterVideoRequest;\n try {\n videoReq = JSON.parse(raw) as OpenRouterVideoRequest;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: `Malformed JSON: ${detail}`,\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return;\n }\n\n // Reject bodies that parsed but are not a JSON object (null, arrays,\n // numbers, strings) before touching any fields — mirrors fal's parseBody\n // guard so callers get a 400 instead of a raw TypeError 500.\n if (videoReq === null || typeof videoReq !== \"object\" || Array.isArray(videoReq)) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: \"Request body must be a JSON object\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n // Field-validation 400s journal the parsed body (unlike the malformed-JSON\n // and non-object paths above, where there is no meaningful object to log).\n const parsedBody = validationJournalBody(videoReq);\n\n if (typeof videoReq.prompt !== \"string\" || !videoReq.prompt) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: parsedBody,\n response: { status: 400, fixture: null },\n });\n // Distinguish an absent prompt from one that is present but unusable\n // (non-string or empty) — \"missing\" would be wrong for the latter. The\n // invalid-type message mirrors the model check below.\n const message =\n videoReq.prompt === undefined\n ? \"Missing required parameter: 'prompt'\"\n : \"Invalid type for parameter: 'prompt' must be a non-empty string\";\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: { message, type: \"invalid_request_error\" },\n }),\n );\n return;\n }\n\n // An empty-string model is as unusable as a non-string one — it matches no\n // fixture and is not a real model id — so both get the same 400.\n if (videoReq.model !== undefined && (typeof videoReq.model !== \"string\" || !videoReq.model)) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: parsedBody,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: \"Invalid type for parameter: 'model' must be a non-empty string\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n const syntheticReq: ChatCompletionRequest = {\n // Model-less submits assume the default model — a fixture restricted to\n // DEFAULT_OPENROUTER_VIDEO_MODEL will match them.\n model: videoReq.model ?? DEFAULT_OPENROUTER_VIDEO_MODEL,\n messages: [{ role: \"user\", content: videoReq.prompt }],\n _endpointType: \"video\",\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n // Match count increments BEFORE applyChaos below by design (mirrors\n // handleCompletions): a chaos-dropped submit still consumes the fixture's\n // sequence slot.\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n const snippet = videoReq.prompt.slice(0, 80);\n defaults.logger.debug(\n `No fixture matched for request (model=${syntheticReq.model}, msg=\"${snippet}\")`,\n );\n }\n\n // Chaos deliberately rolls AFTER body validation and fixture matching\n // (mirrors handleCompletions) — unlike the GET endpoints above, where chaos\n // rolls first. Divergence from the generic record path (server.ts):\n // EVERY firing chaos action here suppresses the would-be proxy entirely —\n // the submit never reaches the upstream and nothing is recorded. On the\n // generic path that is true only for drop/disconnect; a chaos \"malformed\"\n // there still proxies upstream and swaps the relay body via the\n // beforeWriteResponse hook (the fixture records what upstream really\n // said), whereas here \"malformed\" synthesizes a mock body with no\n // upstream contact. The roll is still LABELED \"proxy\" below because that\n // is what the request would have been had chaos not fired.\n if (\n applyChaos(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: syntheticReq },\n // An unmatched submit is proxied upstream when record mode has an\n // openrouter provider configured AND strict would not win (strict 503s\n // before any proxy attempt) — label that chaos roll \"proxy\". A strict\n // no-match, or no record/provider, is served internally.\n fixture\n ? \"fixture\"\n : resolveStrictMode(defaults.strict, req.headers)\n ? \"internal\"\n : defaults.record?.providers.openrouter\n ? \"proxy\"\n : \"internal\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (!fixture) {\n // Strict mode wins over record: a strict no-match must fail loudly with\n // 503 rather than silently recording a new fixture.\n const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n if (effectiveStrict) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n defaults.logger.error(strictNoMatchLogLine(method, path, skippedBySequenceOrTurn));\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\n JSON.stringify({\n error: {\n message: strictMessage,\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n\n if (defaults.record) {\n const outcome = await proxyOpenRouterVideoSubmit({\n req,\n res,\n raw,\n syntheticReq,\n record: defaults.record,\n journal,\n defaults,\n jobs,\n method,\n path,\n });\n if (outcome === \"handled\") return;\n // outcome === \"no_upstream\" — fall through to 404 (fal convention).\n }\n\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 404,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 404,\n JSON.stringify({ error: { message: \"No fixture matched\", code: 404 } }),\n );\n return;\n }\n\n // World-generation snapshot (mirrors the record-submit guard): a fixtures\n // reset can land while the fixture's ResponseFactory below is awaited, and\n // the job insertion at the bottom must not seed the NEW world with this\n // pre-reset submit's job.\n const worldGeneration = jobs.generation;\n const response = await resolveResponse(fixture, syntheticReq);\n\n // Guards BEFORE journaling on every branch below (file convention):\n // resolveResponse awaits the fixture's ResponseFactory, which can be\n // arbitrarily slow — a client that disconnected meanwhile gets neither a\n // write nor a journal entry.\n if (isErrorResponse(response)) {\n if (res.destroyed || res.writableEnded) return;\n const status = response.status ?? 500;\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, serializeErrorResponse(response), {\n retryAfter: response.retryAfter,\n });\n return;\n }\n\n if (!isVideoResponse(response)) {\n if (res.destroyed || res.writableEnded) return;\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: { message: \"Fixture response is not a video type\", type: \"server_error\" },\n }),\n );\n return;\n }\n\n if (res.destroyed || res.writableEnded) return;\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n\n // A fixture authored with any non-terminal status — \"processing\" or a\n // status outside the union entirely (JSON fixtures bypass the compile-time\n // check) — has no terminal state to converge on; terminalStatus coerces it\n // to completed. Keep the behavior (jobs always terminate) but surface the\n // coercion. Widen to string first: the runtime value may not be in the union.\n const fixtureStatus: string = response.video.status;\n if (fixtureStatus === \"processing\") {\n defaults.logger.warn(\n `Video fixture has status \"processing\" — treated as completed for /api/v1/videos jobs`,\n );\n } else if (fixtureStatus !== \"completed\" && fixtureStatus !== \"failed\") {\n defaults.logger.warn(\n `Video fixture has unknown status \"${fixtureStatus}\" — treating as completed for /api/v1/videos jobs`,\n );\n }\n\n const jobId = crypto.randomUUID();\n const progression = resolveProgression(defaults.openRouterVideo);\n const job: OpenRouterVideoReplayJob = {\n kind: \"replay\",\n jobId,\n status: \"pending\",\n pollCount: 0,\n pollsBeforeInProgress: progression.pollsBeforeInProgress,\n pollsBeforeCompleted: progression.pollsBeforeCompleted,\n // Shallow-copy so later mutation of the fixture/factory response object\n // cannot retroactively change an in-flight job's stored video.\n video: { ...response.video },\n };\n // Default 0/0 progression seeds the job terminal at submit (mirrors fal's\n // COMPLETED-on-submit initial status) — content is downloadable with zero\n // polls; the first poll merely reports the already-terminal status. The\n // submit envelope still reports \"pending\" like the real API.\n if (progression.pollsBeforeCompleted === 0) {\n job.status = terminalStatus(job);\n }\n if (jobs.generation === worldGeneration) {\n jobs.set(`${testId}:${jobId}`, job);\n } else {\n // A fixtures reset landed while the ResponseFactory was awaited — the\n // world this submit belongs to is gone. Mirror the record-submit guard:\n // skip the insertion but still relay the envelope below (the client's\n // polls will 404, the same observable outcome as TTL eviction — warned\n // here so the 404s are attributable).\n defaults.logger.warn(\n `OpenRouter video submit resolved after a fixtures reset — not inserting job ${jobId} into the new world (its polls will 404)`,\n );\n }\n\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n id: jobId,\n polling_url: `${requestBase(req, defaults.logger)}/api/v1/videos/${jobId}${testIdSuffix(testId, \"?\")}`,\n status: \"pending\",\n }),\n );\n}\n\n// ─── Record mode: live interactive proxy (submit) ───────────────────────────\n\nconst OPENROUTER_VIDEOS_PATH = \"/api/v1/videos\";\n\n/**\n * Proxy an unmatched video submit to the configured OpenRouter upstream and\n * answer the client with a mock-rewritten envelope: a fresh aimock jobId and\n * a polling_url pointing back at the mock (testId embedded). The upstream\n * lifecycle is then driven interactively by the client's own polls — unlike\n * fal's synchronous queue walk, nothing is polled server-side at submit.\n *\n * Returns \"no_upstream\" when record mode has no openrouter provider URL —\n * the caller falls through to its 404 branch (fal convention).\n */\nasync function proxyOpenRouterVideoSubmit(args: {\n req: http.IncomingMessage;\n res: http.ServerResponse;\n raw: string;\n syntheticReq: ChatCompletionRequest;\n record: RecordConfig;\n journal: Journal;\n defaults: HandlerDefaults;\n jobs: OpenRouterVideoJobMap;\n method: string;\n path: string;\n}): Promise<\"handled\" | \"no_upstream\"> {\n const { req, res, raw, syntheticReq, record, journal, defaults, jobs, method, path } = args;\n\n const upstreamBase = record.providers.openrouter;\n if (!upstreamBase) {\n defaults.logger.warn(`No upstream URL configured for provider \"openrouter\" — cannot proxy`);\n return \"no_upstream\";\n }\n\n const proxyError = (msg: string): \"handled\" => {\n defaults.logger.error(`OpenRouter video submit proxy failed: ${msg}`);\n // Guard BEFORE journaling (file convention): a disconnected client gets\n // neither a write nor a journal entry.\n if (res.destroyed || res.writableEnded) return \"handled\";\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 502,\n fixture: null,\n source: \"proxy\",\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 502,\n JSON.stringify({\n error: { message: `Proxy to upstream failed: ${msg}`, type: \"proxy_error\" },\n }),\n );\n return \"handled\";\n };\n\n let submitUrl: URL;\n let upstreamOrigin: string;\n try {\n submitUrl = resolveUpstreamUrl(upstreamBase, OPENROUTER_VIDEOS_PATH);\n upstreamOrigin = new URL(upstreamBase).origin;\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return proxyError(`Invalid upstream URL: ${upstreamBase} (${msg})`);\n }\n\n defaults.logger.warn(\n `NO FIXTURE MATCH — proxying video submit to ${upstreamBase}${OPENROUTER_VIDEOS_PATH}`,\n );\n\n // World-generation snapshot: a fixtures reset landing during the upstream\n // fetch below clears the job map — the insertion guard after the fetch\n // compares against this value (identity checks cannot help here: the job\n // has not been inserted yet).\n const worldGeneration = jobs.generation;\n\n let fetched: { status: number; contentType: string | null; text: string };\n try {\n const upstreamRes = await fetch(submitUrl, {\n method: \"POST\",\n headers: buildForwardHeaders(req),\n body: raw,\n signal: upstreamTimeoutSignal(record),\n });\n fetched = {\n status: upstreamRes.status,\n contentType: upstreamRes.headers.get(\"content-type\"),\n text: await readEnvelopeText(upstreamRes, record),\n };\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown proxy error\";\n return proxyError(msg);\n }\n\n if (fetched.status === 401 || fetched.status === 403) {\n // Real-API fidelity (mirrors the poll and content paths): an upstream\n // auth rejection is the client's problem (bad or expired Bearer) — relay\n // the upstream status and body verbatim instead of wrapping them in a\n // generic 502 proxy_error. No job is created; a retried submit with\n // fixed credentials proxies again.\n defaults.logger.warn(\n `Upstream rejected the video submit (${fetched.status}) — relaying the upstream status`,\n );\n if (res.destroyed || res.writableEnded) return \"handled\";\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: fetched.status,\n fixture: null,\n source: \"proxy\",\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n res.writeHead(fetched.status, {\n \"Content-Type\": fetched.contentType ?? \"application/json\",\n });\n res.end(fetched.text);\n return \"handled\";\n }\n\n let upstreamJobId: string;\n let envelope: Record<string, unknown>;\n {\n if (fetched.status < 200 || fetched.status >= 300) {\n return proxyError(`Submit ${fetched.status}: ${fetched.text.slice(0, 200)}`);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(fetched.text);\n } catch {\n return proxyError(`Submit returned non-JSON: ${fetched.text.slice(0, 200)}`);\n }\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n return proxyError(\"Submit response is not a JSON object\");\n }\n envelope = parsed as Record<string, unknown>;\n upstreamJobId = String(envelope.id ?? \"\").trim();\n if (!upstreamJobId) {\n return proxyError(\"Submit response missing id\");\n }\n }\n\n // Origin-validate upstream's polling_url before adopting it: every client\n // poll forwards the client's Authorization header to this URL, so an\n // envelope nominating a foreign host must not receive it. Off-origin (or\n // missing/unparseable) polling_urls fall back to the constructed path on\n // the configured provider origin.\n let upstreamPollingUrl = resolveUpstreamUrl(\n upstreamBase,\n `${OPENROUTER_VIDEOS_PATH}/${encodeURIComponent(upstreamJobId)}`,\n ).toString();\n const envPolling = envelope.polling_url;\n if (typeof envPolling === \"string\" && envPolling) {\n try {\n const parsedPolling = new URL(envPolling);\n if (parsedPolling.origin === upstreamOrigin) {\n upstreamPollingUrl = parsedPolling.toString();\n } else {\n defaults.logger.warn(\n `Upstream polling_url origin ${parsedPolling.origin} differs from the configured provider origin ${upstreamOrigin} — using the constructed poll URL instead`,\n );\n }\n } catch {\n defaults.logger.warn(\n `Upstream polling_url is not a valid URL (${String(envPolling).slice(0, 100)}) — using the constructed poll URL instead`,\n );\n }\n }\n\n const testId = getTestId(req);\n const matchRequest = defaults.requestTransform\n ? defaults.requestTransform(syntheticReq)\n : syntheticReq;\n const jobId = crypto.randomUUID();\n const job: OpenRouterVideoRecordJob = {\n kind: \"record\",\n jobId,\n status: \"pending\",\n upstreamJobId,\n upstreamPollingUrl,\n match: buildFixtureMatch(matchRequest, record),\n };\n if (jobs.generation === worldGeneration) {\n jobs.set(`${testId}:${jobId}`, job);\n } else {\n // A fixtures reset landed while the upstream submit was in flight — the\n // world this submit belongs to is gone. Seeding the NEW world with a\n // stale record job would resurrect pre-reset state, so skip the\n // insertion but still relay the envelope below (the client's polls will\n // 404, the same observable outcome as TTL eviction — warned here so the\n // 404s are attributable).\n defaults.logger.warn(\n `OpenRouter video submit for upstream job ${upstreamJobId} completed after a fixtures reset — not inserting the job into the new world (its polls will 404)`,\n );\n }\n\n // Guard BEFORE journaling (file convention): a client that disconnected\n // while the upstream submit was in flight gets neither a write nor a\n // journal entry. The job entry above stays — TTL eviction reaps it.\n if (res.destroyed || res.writableEnded) return \"handled\";\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 200,\n fixture: null,\n source: \"proxy\",\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n\n // Same envelope shape as the replay path: mock jobId, mock polling_url\n // (testId embedded for header-less polls), \"pending\" for API fidelity.\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n id: jobId,\n polling_url: `${requestBase(req, defaults.logger)}/api/v1/videos/${jobId}${testIdSuffix(testId, \"?\")}`,\n status: \"pending\",\n }),\n );\n return \"handled\";\n}\n\n// ─── Record mode: live interactive proxy (status poll + eager capture) ──────\n\n/**\n * Default cap on the DECODED byte size embedded as `b64` in a recorded\n * fixture (32 MB). Override per-server via\n * `record.openRouterVideo.maxContentBytes` (0 = unlimited; negative or\n * non-integer values are treated as the default, with a createServer warn).\n * The cap protects disk AND memory: a capture whose upstream response\n * DECLARES an over-cap Content-Length is skipped without downloading, while\n * a response with no declared length is read as a stream with the byte count\n * enforced during the read — on exceed the download aborts with nothing\n * oversized retained in memory. In both cases the fixture is persisted\n * without `b64` and the same-session job serves the placeholder MP4.\n */\nexport const OPENROUTER_VIDEO_DEFAULT_MAX_CONTENT_BYTES = 32 * 1024 * 1024;\n\n/**\n * Proxy a status poll for a record-mode job 1:1 to the upstream and relay\n * the result with mock-rewritten identifiers (rewriteRecordPollBody: id,\n * polling_url, unsigned_urls; everything else verbatim). When the upstream\n * reports `completed`, the rewritten body is relayed IMMEDIATELY and the\n * eager capture (server-side fetch of unsigned_urls[0], forwarding the\n * polling client's Bearer ONLY same-origin; persist; replay mutation) runs\n * DETACHED in the background — see captureOpenRouterVideoRecordFixture — so\n * an SDK poller is never blocked on a multi-minute download; the content\n * handler (and every later poll) serves the job locally once the capture\n * lands. `failed` persists a failed fixture synchronously on the relaying\n * poll. Every post-await map mutation is identity-guarded\n * (`jobs.get(key) === job`) so a stale response resolving after a concurrent\n * poll's capture replaced the entry can never resurrect the detached record\n * object over the terminal replay job.\n * `cancelled`/`expired` (not representable in VideoResponse.status) pass\n * through with a warn and are never recorded. Under `record.proxyOnly`\n * nothing is captured, persisted, or mutated: terminal polls relay the\n * rewritten upstream body, the upstream unsigned_urls are stored on the\n * record job for the content endpoint to live-proxy, and every later poll\n * keeps proxying upstream. Each successful proxied poll of a record job\n * refreshes its TTL so a long generation cannot be evicted mid-recording.\n */\nasync function proxyOpenRouterVideoRecordPoll(args: {\n req: http.IncomingMessage;\n res: http.ServerResponse;\n job: OpenRouterVideoRecordJob;\n key: string;\n testId: string;\n fixtures: Fixture[];\n journal: Journal;\n defaults: HandlerDefaults;\n jobs: OpenRouterVideoJobMap;\n method: string;\n path: string;\n}): Promise<void> {\n const { req, res, job, key, testId, fixtures, journal, defaults, jobs, method, path } = args;\n const logger = defaults.logger;\n\n const journalProxy = (status: number): void => {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: {\n status,\n fixture: null,\n source: \"proxy\",\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n };\n\n const proxyError = (msg: string): void => {\n logger.error(`OpenRouter video poll proxy failed: ${msg}`);\n // Guard BEFORE journaling (file convention): a disconnected client gets\n // neither a write nor a journal entry.\n if (res.destroyed || res.writableEnded) return;\n journalProxy(502);\n writeErrorResponse(\n res,\n 502,\n JSON.stringify({\n error: { message: `Proxy to upstream failed: ${msg}`, type: \"proxy_error\" },\n }),\n );\n };\n\n // Recording can be disabled mid-flight (LLMock.disableRecording) — an\n // orphaned record job must fail loudly on ANY poll, before contacting the\n // upstream, rather than silently relaying non-terminal statuses and only\n // erroring at the terminal poll.\n const record = defaults.record;\n if (!record) {\n proxyError(\"record mode is no longer configured for an in-flight record job\");\n return;\n }\n\n let fetched: { status: number; contentType: string | null; text: string };\n try {\n const upstreamRes = await fetch(job.upstreamPollingUrl, {\n headers: buildForwardHeaders(req),\n // The locally captured `record` — not defaults.record, which is the\n // same object today but would silently diverge if the defaults were\n // ever swapped between the gate above and this fetch.\n signal: upstreamTimeoutSignal(record),\n });\n fetched = {\n status: upstreamRes.status,\n contentType: upstreamRes.headers.get(\"content-type\"),\n text: await readEnvelopeText(upstreamRes, record),\n };\n } catch (err) {\n proxyError(err instanceof Error ? err.message : \"Unknown proxy error\");\n return;\n }\n\n if (fetched.status === 401 || fetched.status === 403) {\n // Real-API fidelity (mirrors the content path): an upstream auth\n // rejection is the client's problem (bad or expired Bearer) — relay the\n // upstream status and body verbatim instead of wrapping them in a\n // generic 502 proxy_error. The job is untouched: a later poll with fixed\n // credentials proxies again.\n logger.warn(\n `Upstream rejected the status poll for job ${job.upstreamJobId} (${fetched.status}) — relaying the upstream status`,\n );\n if (res.destroyed || res.writableEnded) return;\n journalProxy(fetched.status);\n res.writeHead(fetched.status, {\n \"Content-Type\": fetched.contentType ?? \"application/json\",\n });\n res.end(fetched.text);\n return;\n }\n\n let upstreamBody: Record<string, unknown>;\n {\n if (fetched.status < 200 || fetched.status >= 300) {\n proxyError(`Status ${fetched.status}: ${fetched.text.slice(0, 200)}`);\n return;\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(fetched.text);\n } catch {\n proxyError(`Status returned non-JSON: ${fetched.text.slice(0, 200)}`);\n return;\n }\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n proxyError(\"Status response is not a JSON object\");\n return;\n }\n upstreamBody = parsed as Record<string, unknown>;\n }\n\n const relayJson = (body: Record<string, unknown>): void => {\n // Guard BEFORE journaling: a client that disconnected while the upstream\n // poll was in flight gets neither a write nor a journal entry — the\n // journaled 200 must reflect a response that actually left.\n if (res.destroyed || res.writableEnded) return;\n journalProxy(200);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n };\n\n // Mock-rewritten relay body — used by EVERY branch below so no upstream\n // URL-bearing field (polling_url, unsigned_urls) can escape the mock.\n const relayBody = rewriteRecordPollBody({ upstreamBody, job, req, testId, logger });\n\n const upstreamStatus = String(upstreamBody.status ?? \"\");\n\n if (upstreamStatus === \"pending\" || upstreamStatus === \"in_progress\") {\n // Identity guard on EVERY post-await mutation/re-insert: the upstream\n // fetch above may have resolved AFTER a concurrent poll's capture\n // replaced the map entry with a terminal replay job (or after TTL\n // eviction). Mutating or re-inserting the detached dispatch-time\n // reference would regress the terminal job back to a live proxy — the\n // body is still relayed either way.\n if (jobs.get(key) === job) {\n // Identity alone is NOT enough during the capture window (and under\n // proxyOnly after a terminal poll): the SAME record object stays in\n // the map with status \"completed\"/\"failed\", so a stale non-terminal\n // response resolving late would regress it and reopen the content\n // endpoint's 400 window. Skip the status write once capturing or\n // terminal; the TTL refresh below still applies.\n if (!job.capturing && job.status !== \"completed\" && job.status !== \"failed\") {\n job.status = upstreamStatus;\n }\n // Refresh the TTL: an actively-polled record job must not be evicted\n // mid-recording however long the generation takes.\n jobs.set(key, job);\n }\n relayJson(relayBody);\n return;\n }\n\n if (upstreamStatus === \"completed\") {\n if (record.proxyOnly) {\n // Proxy-only: no fixtures, no in-memory caching, no replay mutation.\n // The job stays kind:\"record\"; the upstream unsigned_urls are stored so\n // the content endpoint can live-proxy the bytes (id mapping is\n // inherently stateful — the bytes never are). Stored UNFILTERED so the\n // live proxy's indexes stay aligned with the rewritten relay's.\n // Identity-guarded like every post-await mutation in this function.\n if (jobs.get(key) === job) {\n const urls = upstreamBody.unsigned_urls;\n job.status = \"completed\";\n // Refresh the stash only from an array body (the capture-window twin\n // below guards this exact case): a later completed poll that omits or\n // corrupts unsigned_urls must not clobber a usable stash with\n // undefined.\n if (Array.isArray(urls)) {\n job.upstreamUnsignedUrls = [...urls];\n }\n jobs.set(key, job); // TTL refresh\n }\n relayJson(relayBody);\n return;\n }\n\n if (job.capturing || jobs.get(key) !== job) {\n // A concurrent poll already entered the capture sequence (capturing\n // latched), or this dispatch-time reference is detached — the map entry\n // was replaced by the finished capture's replay job (or TTL-evicted)\n // while the upstream fetch above was in flight. Relay the rewritten\n // body either way, but NEVER re-insert the stale object (it would\n // resurrect a permanent live proxy OVER the terminal replay job) and\n // never start a second capture/persist.\n if (jobs.get(key) === job) {\n // Refresh the stored upstream URLs from THIS poll's body (mirroring\n // the proxyOnly branch): upstream unsigned URLs can rotate between\n // polls (signed-by-time CDN links), so in-window content\n // live-proxies must use the freshest set. Non-array bodies keep the\n // existing stash — a defective later poll must not clobber a usable\n // one.\n const freshUrls = upstreamBody.unsigned_urls;\n if (Array.isArray(freshUrls)) {\n job.upstreamUnsignedUrls = [...freshUrls];\n }\n jobs.set(key, job); // TTL refresh, like every other successful proxied poll\n }\n relayJson(relayBody);\n return;\n }\n const urls = upstreamBody.unsigned_urls;\n // Open the capturing window SYNCHRONOUSLY, before the first await of the\n // capture sequence: `capturing` is the double-capture guard, and the\n // terminal status + stashed upstream unsigned_urls make the job fully\n // observable as completed while the capture is in flight — a client that\n // follows this (or a concurrent) poll's relayed content URL gets a\n // live-proxied download instead of a 400.\n job.capturing = true;\n job.status = \"completed\";\n // Refresh the stash only from an array body (the proxyOnly and\n // capture-window siblings above guard this exact case): a later completed\n // poll that omits or corrupts unsigned_urls must not clobber a usable\n // stash with undefined.\n if (Array.isArray(urls)) {\n job.upstreamUnsignedUrls = [...urls];\n }\n jobs.set(key, job); // TTL refresh — identity-checked above, no await since\n\n // Relay the completed body IMMEDIATELY: a real video download can take\n // minutes, and an SDK poller blocked on it would time out. The capture\n // (download + persist + replay mutation) runs DETACHED below; the\n // capturing window keeps the job serving correctly meanwhile.\n relayJson(relayBody);\n\n // Detached eager capture. Handles every failure internally and never\n // rejects; closes the capturing window in its finally.\n void captureOpenRouterVideoRecordFixture({\n req,\n job,\n key,\n testId,\n fixtures,\n defaults,\n jobs,\n record,\n upstreamBody,\n });\n return;\n }\n\n if (upstreamStatus === \"failed\") {\n if (record.proxyOnly) {\n // Proxy-only: relay the rewritten failure body, persist nothing, keep\n // the job a live proxy. Identity-guarded like every post-await\n // mutation in this function.\n if (jobs.get(key) === job) {\n job.status = \"failed\";\n jobs.set(key, job); // TTL refresh\n }\n relayJson(relayBody);\n return;\n }\n\n if (jobs.get(key) !== job) {\n // Concurrency guard: a concurrent poll at \"failed\" already persisted\n // the failure fixture and replaced the entry while this poll's\n // upstream fetch was in flight (everything below the fetch is\n // synchronous, so the first poll to resume wins atomically). Relay\n // without persisting a duplicate fixture or re-registering it.\n // This identity check ALSO covers a fixtures reset landing during the\n // upstream fetch: performFixturesReset clears the job map, so a\n // cleared world fails the check and the stale failure fixture never\n // pollutes the next world's fixtures array — valid because everything\n // from here to persistFixture below is synchronous (no interleaving\n // point between check and persist).\n relayJson(relayBody);\n return;\n }\n\n const rawError = upstreamBody.error;\n let error: string | undefined;\n if (typeof rawError === \"string\" && rawError) {\n error = rawError;\n } else if (rawError !== null && typeof rawError === \"object\" && !Array.isArray(rawError)) {\n // Canonical envelope shape ({ error: { message, code } }) — extract the\n // message like the recorder's error-fixture detection does.\n const message = (rawError as Record<string, unknown>).message;\n if (typeof message === \"string\" && message) {\n error = message;\n }\n }\n if (error === undefined && rawError !== undefined && rawError !== null) {\n logger.warn(\n `Upstream video job ${job.upstreamJobId} failed with an unusable error value (${JSON.stringify(rawError).slice(0, 100)}) — recording the fixture without an error message (replay serves the default)`,\n );\n }\n const video: VideoResponse[\"video\"] = {\n id: job.upstreamJobId,\n status: \"failed\",\n ...(error !== undefined ? { error } : {}),\n };\n const persistResult = persistFixture({\n record,\n providerKey: \"openrouter\",\n testId,\n fixture: { match: job.match, response: { video } },\n fixtures,\n logger,\n });\n if (persistResult.kind === \"failed\" && !res.headersSent) {\n res.setHeader(\"X-AIMock-Record-Error\", sanitizeHeaderValue(persistResult.error));\n }\n jobs.set(key, {\n kind: \"replay\",\n jobId: job.jobId,\n status: \"failed\",\n pollCount: 0,\n pollsBeforeInProgress: 0,\n pollsBeforeCompleted: 0,\n video: { ...video },\n });\n // Passthrough of the upstream failure body, identifiers rewritten.\n relayJson(relayBody);\n return;\n }\n\n // cancelled / expired / anything else: terminal upstream states that are\n // not representable in VideoResponse.status — pass through (identifiers\n // rewritten), record nothing, and keep proxying any further polls live.\n logger.warn(\n `Upstream video job ${job.upstreamJobId} reported status \"${upstreamStatus}\" — not representable in fixture video.status; passing through without recording`,\n );\n // Identity-guarded TTL refresh — the job keeps proxying live, but a\n // detached reference is never re-inserted over a replaced entry.\n if (jobs.get(key) === job) {\n jobs.set(key, job);\n }\n relayJson(relayBody);\n}\n\n/**\n * Detached eager-capture sequence for a completed record job. Runs AFTER the\n * triggering completed poll has been relayed (the client is never blocked on\n * a potentially multi-minute video download): fetches `unsigned_urls[0]`\n * server-side (the polling client's Bearer is forwarded ONLY same-origin),\n * persists the fixture, and mutates the map entry into a terminal replay job.\n * Every failure is handled internally and the returned promise NEVER\n * rejects. Failure semantics: a capture FAILURE — no usable\n * unsigned_urls, an invalid content URL, or a content fetch that errors on\n * headers, status, or body — persists NOTHING and leaves the job a live\n * proxy, so the next completed poll retries the capture (each failure\n * warns). The ONLY degraded persist is the over-cap path (declared or\n * streamed): retrying would always re-exceed the cap, so the b64-less\n * fixture + placeholder is the deliberate, permanent outcome. The capturing\n * window opened by the caller is closed in the finally when the map still\n * holds this record job; on the success path the entry was just replaced and\n * the detached record object deliberately KEEPS capturing=true so a\n * concurrent poll that read it before the replacement still early-relays\n * instead of starting a second capture.\n */\nasync function captureOpenRouterVideoRecordFixture(args: {\n req: http.IncomingMessage;\n job: OpenRouterVideoRecordJob;\n key: string;\n testId: string;\n fixtures: Fixture[];\n defaults: HandlerDefaults;\n jobs: OpenRouterVideoJobMap;\n record: RecordConfig;\n upstreamBody: Record<string, unknown>;\n}): Promise<void> {\n const { req, job, key, testId, fixtures, defaults, jobs, record, upstreamBody } = args;\n const logger = defaults.logger;\n const urls = upstreamBody.unsigned_urls;\n\n const warnings: string[] = [];\n let capturedB64: string | undefined;\n try {\n const usage = upstreamBody.usage;\n const rawCost =\n usage !== null && typeof usage === \"object\" && !Array.isArray(usage)\n ? (usage as Record<string, unknown>).cost\n : undefined;\n const cost = typeof rawCost === \"number\" ? rawCost : undefined;\n\n // Sanitized like the poll-threshold configs: a negative/non-integer cap\n // is treated as the default (createServer warns at startup).\n const rawCap = record.openRouterVideo?.maxContentBytes;\n const cap =\n rawCap !== undefined && Number.isInteger(rawCap) && rawCap >= 0\n ? rawCap\n : OPENROUTER_VIDEO_DEFAULT_MAX_CONTENT_BYTES;\n\n const urlArray = Array.isArray(urls) ? urls : undefined;\n if (urlArray && urlArray.length > 1) {\n logger.warn(\n `Upstream video job ${job.upstreamJobId} reported ${urlArray.length} unsigned_urls — only index 0 is captured; post-capture replays serve the index-0 bytes for every index`,\n );\n }\n const firstRaw = urlArray?.[0];\n const firstUrl = typeof firstRaw === \"string\" && firstRaw ? firstRaw : undefined;\n if (!firstUrl) {\n // Capture FAILURE: persist nothing, leave the job a live\n // proxy (the finally clears `capturing`) so the next completed poll —\n // whose upstream body may carry usable URLs — retries. Distinguish\n // \"no unsigned_urls at all\" from \"unsigned_urls present but [0] is\n // unusable\" (non-string, or an empty string) — the two point at\n // different upstream defects.\n if (urlArray && urlArray.length > 0) {\n logger.warn(\n `Upstream video job ${job.upstreamJobId} completed with an unusable unsigned_urls[0] (${JSON.stringify(firstRaw).slice(0, 100)}) — capture skipped, nothing persisted; the next completed poll retries`,\n );\n } else {\n logger.warn(\n `Upstream video job ${job.upstreamJobId} completed without unsigned_urls — capture skipped, nothing persisted; the next completed poll retries`,\n );\n }\n return;\n }\n let contentUrl: URL;\n try {\n contentUrl = new URL(firstUrl);\n } catch {\n // Capture failure — persist nothing, retry on the next completed poll.\n logger.warn(\n `Upstream unsigned_urls[0] is not a valid URL (${firstUrl.slice(0, 100)}) — capture skipped, nothing persisted; the next completed poll retries`,\n );\n return;\n }\n {\n // The client's Bearer is forwarded ONLY to the configured provider\n // origin — an off-origin content host (CDN or a hostile envelope)\n // must not receive it.\n const providerBase = record.providers.openrouter;\n let providerOrigin: string | undefined;\n try {\n providerOrigin = new URL(providerBase ?? job.upstreamPollingUrl).origin;\n } catch {\n try {\n providerOrigin = new URL(job.upstreamPollingUrl).origin;\n } catch {\n // Double parse failure (unreachable in practice — the polling URL\n // was origin-validated at submit). The relay has already left, so\n // there is no response to 502 — log, persist nothing, and let the\n // finally close the capturing window (the job stays a live\n // proxy; a later poll retries the capture).\n logger.error(\n `OpenRouter video capture for job ${job.upstreamJobId} aborted: cannot determine the provider origin (invalid provider URL and polling URL) — refusing to forward credentials`,\n );\n return;\n }\n }\n const headers = buildForwardHeaders(req);\n if (contentUrl.origin !== providerOrigin) {\n delete headers.authorization;\n logger.warn(\n `Upstream unsigned_urls[0] origin ${contentUrl.origin} differs from the provider origin ${providerOrigin} — fetching content WITHOUT the client's Authorization header`,\n );\n }\n try {\n // Headers gated by upstreamTimeoutMs; the body streams under\n // bodyTimeoutMs IDLE semantics so a long steady download (a >30s\n // video render) is never aborted by a total deadline.\n const contentRes = await fetchHeadersWithTimeout(contentUrl, headers, record);\n if (!contentRes.ok) {\n // Bounded body sample (idle semantics + the error cap, like the\n // content proxy's failure path) — a bare status is not diagnosable.\n let sample = \"\";\n try {\n const read = await readBodyIdle(contentRes, record, OPENROUTER_VIDEO_ERROR_BODY_CAP);\n if (!read.overCap) sample = read.buf.toString(\"utf8\").slice(0, 200);\n } catch {\n // Body unreadable — the status alone still names the failure.\n }\n throw new Error(`Content ${contentRes.status}${sample ? `: ${sample}` : \"\"}`);\n }\n const declaredLength = Number(contentRes.headers.get(\"content-length\") ?? NaN);\n if (cap > 0 && Number.isFinite(declaredLength) && declaredLength > cap) {\n // Memory guard: the upstream DECLARED an over-cap size — skip the\n // download entirely instead of buffering it just to discard it.\n // Over-cap is the ONE degraded persist (see the function doc): a\n // retry would always re-exceed the cap, so the b64-less fixture is\n // deliberate and permanent.\n void contentRes.body?.cancel().catch(() => {});\n logger.warn(\n `Captured video for job ${job.upstreamJobId} declares Content-Length ${declaredLength} — over maxContentBytes (${cap}); skipping the download; fixture persisted WITHOUT b64 (content serves the placeholder MP4)`,\n );\n warnings.push(\n `Declared content length (${declaredLength} bytes) exceeded maxContentBytes (${cap}) — download skipped, b64 omitted`,\n );\n } else {\n // The cap is enforced DURING the streamed read: on exceed the\n // read aborts and nothing oversized is retained in memory —\n // the same-session job serves the placeholder, exactly like\n // the declared-length skip above (the same deliberate over-cap\n // persist).\n const read = await readBodyIdle(contentRes, record, cap);\n if (read.overCap) {\n logger.warn(\n `Captured video for job ${job.upstreamJobId} streamed past maxContentBytes (${cap}) — download aborted at ${read.bytesRead} bytes; fixture persisted WITHOUT b64 (content serves the placeholder MP4)`,\n );\n warnings.push(\n `Captured video exceeded maxContentBytes (${cap}) — download aborted, b64 omitted`,\n );\n } else {\n capturedB64 = read.buf.toString(\"base64\");\n }\n }\n } catch (err) {\n // Capture failure (headers, status, or body) — transient by nature:\n // persist nothing, leave the job live, warn; the next completed poll\n // retries (was: persist a b64-less fixture forever).\n const msg = err instanceof Error ? err.message : \"Unknown capture error\";\n logger.warn(\n `OpenRouter video content capture failed (${msg}) — nothing persisted; the job keeps proxying live and the next completed poll retries`,\n );\n return;\n }\n }\n\n const video: VideoResponse[\"video\"] = {\n id: job.upstreamJobId,\n status: \"completed\",\n ...(capturedB64 !== undefined ? { b64: capturedB64 } : {}),\n ...(cost !== undefined ? { cost } : {}),\n };\n // World-generation guard: a fixtures reset (POST\n // /__aimock/reset/fixtures) landing during the multi-second capture\n // above clears BOTH the fixtures array and the job map\n // (performFixturesReset) — so map identity is a valid proxy for \"same\n // world\": if `jobs.get(key)` no longer returns this job, the world this\n // capture belongs to is gone and persisting would push a stale fixture\n // into the NEXT world's array (and write a file the new world never\n // asked for). Checked immediately before persistFixture with no await\n // in between.\n if (jobs.get(key) !== job) {\n logger.warn(\n `OpenRouter video capture for job ${job.upstreamJobId} discarded: the job map no longer holds this job (fixtures reset or TTL eviction mid-capture) — nothing persisted`,\n );\n return;\n }\n // A persist failure cannot ride an X-AIMock-Record-Error header here —\n // the relay left before the capture started (the failed branch, which\n // persists synchronously, still sets the header). persistFixture logs\n // the failure; the replay mutation below still happens so the in-memory\n // session serves the captured bytes.\n persistFixture({\n record,\n providerKey: \"openrouter\",\n testId,\n fixture: { match: job.match, response: { video } },\n fixtures,\n warnings,\n logger,\n });\n\n // Mutate the map entry into a terminal replay job: later polls and the\n // content handler serve it locally from here on. An over-cap capture\n // never keeps bytes in memory (the streamed read aborted at the cap),\n // so the replayed content is the placeholder MP4. Identity-guarded: a\n // TTL-evicted (or otherwise replaced) entry is never resurrected.\n if (jobs.get(key) === job) {\n jobs.set(key, {\n kind: \"replay\",\n jobId: job.jobId,\n status: \"completed\",\n pollCount: 0,\n pollsBeforeInProgress: 0,\n pollsBeforeCompleted: 0,\n video: { ...video },\n });\n }\n } catch (err) {\n // Nothing may reject unhandled out of the detached capture. An\n // unexpected throw (every expected failure is handled above) leaves the\n // job a live proxy; a later completed poll retries the capture.\n const msg = err instanceof Error ? err.message : String(err);\n logger.error(\n `OpenRouter video capture for job ${job.upstreamJobId} failed unexpectedly (${msg}) — fixture not persisted; the job keeps proxying live`,\n );\n } finally {\n // Close the capturing window when the map still holds this record job\n // (a capture step threw before the replay mutation) — a latched\n // `capturing` would make every later poll early-relay and the job\n // could never be captured. On the success path the entry was just\n // replaced; the detached record object deliberately KEEPS\n // capturing=true (see the function doc).\n if (jobs.get(key) === job) {\n job.capturing = false;\n }\n }\n}\n"],"mappings":"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"}
package/dist/recorder.cjs CHANGED
@@ -1,9 +1,9 @@
1
1
  const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
2
2
  const require_constants = require('./constants.cjs');
3
+ const require_model_utils = require('./model-utils.cjs');
3
4
  const require_helpers = require('./helpers.cjs');
4
5
  const require_router = require('./router.cjs');
5
6
  const require_sse_writer = require('./sse-writer.cjs');
6
- const require_model_utils = require('./model-utils.cjs');
7
7
  const require_stream_collapse = require('./stream-collapse.cjs');
8
8
  const require_url = require('./url.cjs');
9
9
  let node_http = require("node:http");
@@ -32,9 +32,39 @@ const STRIP_HEADERS = new Set([
32
32
  "host",
33
33
  "content-length",
34
34
  "cookie",
35
- "accept-encoding"
35
+ "accept-encoding",
36
+ "x-test-id",
37
+ "x-aimock-strict",
38
+ "x-aimock-context"
36
39
  ]);
37
40
  /**
41
+ * Build the header set forwarded to an upstream provider from an incoming
42
+ * request: everything except hop-by-hop, client-set, and mock-internal
43
+ * headers (STRIP_HEADERS, plus the x-aimock-chaos-* prefix family). Shared
44
+ * by the generic recorder proxy and the OpenRouter-video live lifecycle
45
+ * proxy.
46
+ */
47
+ function buildForwardHeaders(req) {
48
+ const out = {};
49
+ for (const [name, val] of Object.entries(req.headers)) {
50
+ const lower = name.toLowerCase();
51
+ if (val === void 0 || STRIP_HEADERS.has(lower) || lower.startsWith("x-aimock-chaos-")) continue;
52
+ out[name] = Array.isArray(val) ? val.join(", ") : val;
53
+ }
54
+ return out;
55
+ }
56
+ /**
57
+ * Make an arbitrary string safe to set as an HTTP header value. Node's
58
+ * `res.setHeader` throws ERR_INVALID_CHAR on anything outside Latin-1 (plus
59
+ * control characters) — and persist errors embed filesystem paths verbatim,
60
+ * so a Unicode fixture path would turn a recoverable record failure into a
61
+ * 500. Out-of-range characters are replaced with `?` rather than stripped so
62
+ * the value's shape stays legible.
63
+ */
64
+ function sanitizeHeaderValue(value) {
65
+ return value.replace(/[^\t\x20-\x7e\x80-\xff]/g, "?");
66
+ }
67
+ /**
38
68
  * Write a built fixture to disk (snapshot vs. timestamp file layout) and, when
39
69
  * the match is non-empty, register it in the in-memory cache so subsequent
40
70
  * identical requests match. Extracted from `proxyAndRecord` so the fal
@@ -46,11 +76,11 @@ function persistFixture(opts) {
46
76
  const { record, providerKey, testId, fixture, fixtures, warnings = [], logger } = opts;
47
77
  const m = fixture.match;
48
78
  const isEmptyMatch = m.userMessage === void 0 && m.inputText === void 0 && m.endpoint === void 0;
49
- if (isEmptyMatch) logger.warn("Recorded fixture has empty match criteria — skipping in-memory registration");
50
79
  if (record.proxyOnly) {
51
80
  logger.info(`Proxied ${providerKey} request (proxy-only mode)`);
52
81
  return { kind: "skipped" };
53
82
  }
83
+ if (isEmptyMatch) logger.warn("Recorded fixture has empty match criteria — skipping in-memory registration");
54
84
  const fixturePath = record.fixturePath ?? "./fixtures/recorded";
55
85
  let isSnapshotMode = testId !== require_constants.DEFAULT_TEST_ID;
56
86
  let filepath;
@@ -74,14 +104,18 @@ function persistFixture(opts) {
74
104
  node_fs.mkdirSync(node_path.dirname(filepath), { recursive: true });
75
105
  let fileContent;
76
106
  if (mergeExisting && node_fs.existsSync(filepath)) try {
77
- fileContent = { fixtures: [...JSON.parse(node_fs.readFileSync(filepath, "utf-8")).fixtures ?? [], fixture] };
107
+ const existing = JSON.parse(node_fs.readFileSync(filepath, "utf-8"));
108
+ const existingFixtures = Array.isArray(existing.fixtures) ? existing.fixtures : void 0;
109
+ if (existingFixtures === void 0 && existing.fixtures !== void 0) logger.warn(`Existing fixture file ${filepath} has a non-array "fixtures" — discarding it and starting fresh`);
110
+ fileContent = { fixtures: [...existingFixtures ?? [], fixture] };
111
+ if (typeof existing._warning === "string" && existing._warning) fileWarnings.unshift(...existing._warning.split("; "));
78
112
  } catch (mergeErr) {
79
113
  const msg = mergeErr instanceof Error ? mergeErr.message : "unknown";
80
114
  logger.warn(`Could not read existing fixture file ${filepath} (${msg}) — overwriting`);
81
115
  fileContent = { fixtures: [fixture] };
82
116
  }
83
117
  else fileContent = { fixtures: [fixture] };
84
- if (fileWarnings.length > 0) fileContent._warning = fileWarnings.join("; ");
118
+ if (fileWarnings.length > 0) fileContent._warning = [...new Set(fileWarnings)].join("; ");
85
119
  const tmpPath = filepath + ".tmp." + process.pid;
86
120
  node_fs.writeFileSync(tmpPath, JSON.stringify(fileContent, null, 2), "utf-8");
87
121
  node_fs.renameSync(tmpPath, filepath);
@@ -126,8 +160,7 @@ async function proxyAndRecord(req, res, request, providerKey, pathname, fixtures
126
160
  return "relayed";
127
161
  }
128
162
  defaults.logger.warn(`NO FIXTURE MATCH — proxying to ${upstreamUrl}${pathname}`);
129
- const forwardHeaders = {};
130
- for (const [name, val] of Object.entries(req.headers)) if (val !== void 0 && !STRIP_HEADERS.has(name)) forwardHeaders[name] = Array.isArray(val) ? val.join(", ") : val;
163
+ const forwardHeaders = buildForwardHeaders(req);
131
164
  const requestBody = rawBody ?? JSON.stringify(request);
132
165
  let upstreamStatus;
133
166
  let upstreamHeaders;
@@ -198,14 +231,22 @@ async function proxyAndRecord(req, res, request, providerKey, pathname, fixtures
198
231
  } else if (collapsed.content === "" && (!collapsed.toolCalls || collapsed.toolCalls.length === 0)) {
199
232
  defaults.logger.warn("Stream collapse produced empty content — fixture may be incomplete");
200
233
  const reasoningSpread = collapsed.reasoning ? { reasoning: collapsed.reasoning } : {};
234
+ const reasoningSignatureSpread = collapsed.reasoning && collapsed.reasoningSignature ? { reasoningSignature: collapsed.reasoningSignature } : {};
235
+ logDroppedReasoningSignature(defaults.logger, collapsed.reasoning, collapsed.reasoningSignature);
236
+ const redactedThinkingSpread = collapsed.redactedThinking?.length ? { redactedThinking: collapsed.redactedThinking } : {};
201
237
  const webSearchesSpread = collapsed.webSearches?.length ? { webSearches: collapsed.webSearches } : {};
202
238
  fixtureResponse = {
203
239
  content: collapsed.content ?? "",
204
240
  ...reasoningSpread,
241
+ ...reasoningSignatureSpread,
242
+ ...redactedThinkingSpread,
205
243
  ...webSearchesSpread
206
244
  };
207
245
  } else {
208
246
  const reasoningSpread = collapsed.reasoning ? { reasoning: collapsed.reasoning } : {};
247
+ const reasoningSignatureSpread = collapsed.reasoning && collapsed.reasoningSignature ? { reasoningSignature: collapsed.reasoningSignature } : {};
248
+ logDroppedReasoningSignature(defaults.logger, collapsed.reasoning, collapsed.reasoningSignature);
249
+ const redactedThinkingSpread = collapsed.redactedThinking?.length ? { redactedThinking: collapsed.redactedThinking } : {};
209
250
  const webSearchesSpread = collapsed.webSearches?.length ? { webSearches: collapsed.webSearches } : {};
210
251
  if (collapsed.toolCalls && collapsed.toolCalls.length > 0) {
211
252
  const sanitizedToolCalls = collapsed.toolCalls.map((tc) => ({
@@ -217,16 +258,22 @@ async function proxyAndRecord(req, res, request, providerKey, pathname, fixtures
217
258
  content: collapsed.content,
218
259
  toolCalls: sanitizedToolCalls,
219
260
  ...reasoningSpread,
261
+ ...reasoningSignatureSpread,
262
+ ...redactedThinkingSpread,
220
263
  ...webSearchesSpread
221
264
  };
222
265
  else fixtureResponse = {
223
266
  toolCalls: sanitizedToolCalls,
224
267
  ...reasoningSpread,
268
+ ...reasoningSignatureSpread,
269
+ ...redactedThinkingSpread,
225
270
  ...webSearchesSpread
226
271
  };
227
272
  } else fixtureResponse = {
228
273
  content: collapsed.content ?? "",
229
274
  ...reasoningSpread,
275
+ ...reasoningSignatureSpread,
276
+ ...redactedThinkingSpread,
230
277
  ...webSearchesSpread
231
278
  };
232
279
  }
@@ -261,7 +308,7 @@ async function proxyAndRecord(req, res, request, providerKey, pathname, fixtures
261
308
  } catch (err) {
262
309
  defaults.logger.debug(`Could not parse encoding_format from raw body: ${err instanceof Error ? err.message : "unknown error"}`);
263
310
  }
264
- fixtureResponse = buildFixtureResponse(parsedResponse, upstreamStatus, encodingFormat);
311
+ fixtureResponse = buildFixtureResponse(parsedResponse, upstreamStatus, encodingFormat, defaults.logger);
265
312
  }
266
313
  }
267
314
  if (clientDisconnected) {
@@ -297,7 +344,7 @@ async function proxyAndRecord(req, res, request, providerKey, pathname, fixtures
297
344
  logger: defaults.logger
298
345
  });
299
346
  if (persistResult.kind === "failed") {
300
- if (!res.headersSent) res.setHeader("X-AIMock-Record-Error", persistResult.error);
347
+ if (!res.headersSent) res.setHeader("X-AIMock-Record-Error", sanitizeHeaderValue(persistResult.error));
301
348
  else defaults.logger.warn(`Cannot set X-AIMock-Record-Error header — headers already sent`);
302
349
  defaults.logger.warn(`Response relayed but NOT saved to disk — see error above`);
303
350
  }
@@ -354,6 +401,11 @@ var StreamingFrameDecoder = class {
354
401
  return this.decoder.end();
355
402
  }
356
403
  };
404
+ /**
405
+ * Sanitize a configured timeout: non-finite or non-positive values fall back
406
+ * to the default. Shared with the OpenRouter-video live lifecycle proxy so
407
+ * `record.upstreamTimeoutMs` is clamped identically on every proxy path.
408
+ */
357
409
  function clampTimeout(value, fallback) {
358
410
  if (value == null || !Number.isFinite(value) || value <= 0) return fallback;
359
411
  return value;
@@ -467,10 +519,19 @@ function makeUpstreamRequest(target, headers, body, clientRes, method = "POST",
467
519
  });
468
520
  }
469
521
  /**
522
+ * A captured Anthropic thinking-block signature is only persisted alongside
523
+ * non-empty plaintext `reasoning` (a bare signature has nothing to attach to on
524
+ * replay). When that gate drops a present signature, warn so the loss is
525
+ * observable (matching the recording-side anomaly convention in this file).
526
+ */
527
+ function logDroppedReasoningSignature(logger, reasoning, reasoningSignature) {
528
+ if (reasoningSignature && !reasoning) logger?.warn("Dropping captured reasoningSignature — no plaintext reasoning to attach it to");
529
+ }
530
+ /**
470
531
  * Detect the response format from the parsed upstream JSON and convert
471
532
  * it into an aimock FixtureResponse.
472
533
  */
473
- function buildFixtureResponse(parsed, status, encodingFormat) {
534
+ function buildFixtureResponse(parsed, status, encodingFormat, logger) {
474
535
  if (parsed === null || parsed === void 0) return {
475
536
  error: {
476
537
  message: "Upstream returned non-JSON response",
@@ -599,10 +660,20 @@ function buildFixtureResponse(parsed, status, encodingFormat) {
599
660
  const toolUseBlocks = blocks.filter((b) => b.type === "tool_use");
600
661
  const textBlocks = blocks.filter((b) => b.type === "text" && typeof b.text === "string");
601
662
  const thinkingBlocks = blocks.filter((b) => b.type === "thinking");
663
+ const redactedBlocks = blocks.filter((b) => b.type === "redacted_thinking");
664
+ const redactedThinking = blocks.map((b) => require_stream_collapse.capturedRedactedData(b)).filter((data) => data !== void 0);
602
665
  const hasToolCalls = toolUseBlocks.length > 0;
603
666
  const joinedText = textBlocks.map((b) => String(b.text ?? "")).join("");
604
667
  const hasContent = joinedText.length > 0;
605
668
  const anthropicReasoning = thinkingBlocks.length > 0 ? thinkingBlocks.map((b) => String(b.thinking ?? "")).join("") : void 0;
669
+ const anthropicReasoningSignature = (() => {
670
+ let sig;
671
+ for (const b of thinkingBlocks) if (typeof b.signature === "string") sig = String(b.signature);
672
+ return sig;
673
+ })();
674
+ const reasoningSignatureSpread = anthropicReasoning && anthropicReasoningSignature ? { reasoningSignature: anthropicReasoningSignature } : {};
675
+ logDroppedReasoningSignature(logger, anthropicReasoning, anthropicReasoningSignature);
676
+ const redactedThinkingSpread = redactedThinking.length > 0 ? { redactedThinking } : {};
606
677
  if (hasToolCalls) {
607
678
  const toolCalls = toolUseBlocks.map((b) => ({
608
679
  name: String(b.name),
@@ -612,20 +683,28 @@ function buildFixtureResponse(parsed, status, encodingFormat) {
612
683
  if (hasContent) return {
613
684
  content: joinedText,
614
685
  toolCalls,
615
- ...anthropicReasoning ? { reasoning: anthropicReasoning } : {}
686
+ ...anthropicReasoning ? { reasoning: anthropicReasoning } : {},
687
+ ...reasoningSignatureSpread,
688
+ ...redactedThinkingSpread
616
689
  };
617
690
  return {
618
691
  toolCalls,
619
- ...anthropicReasoning ? { reasoning: anthropicReasoning } : {}
692
+ ...anthropicReasoning ? { reasoning: anthropicReasoning } : {},
693
+ ...reasoningSignatureSpread,
694
+ ...redactedThinkingSpread
620
695
  };
621
696
  }
622
697
  if (hasContent) return {
623
698
  content: joinedText,
624
- ...anthropicReasoning ? { reasoning: anthropicReasoning } : {}
699
+ ...anthropicReasoning ? { reasoning: anthropicReasoning } : {},
700
+ ...reasoningSignatureSpread,
701
+ ...redactedThinkingSpread
625
702
  };
626
- if (anthropicReasoning) return {
703
+ if (thinkingBlocks.length > 0 || redactedBlocks.length > 0) return {
627
704
  content: "",
628
- reasoning: anthropicReasoning
705
+ ...anthropicReasoning ? { reasoning: anthropicReasoning } : {},
706
+ ...reasoningSignatureSpread,
707
+ ...redactedThinkingSpread
629
708
  };
630
709
  }
631
710
  if (Array.isArray(obj.candidates) && obj.candidates.length > 0) {
@@ -823,6 +902,9 @@ function buildFixtureMetadata(request) {
823
902
 
824
903
  //#endregion
825
904
  exports.buildFixtureMatch = buildFixtureMatch;
905
+ exports.buildForwardHeaders = buildForwardHeaders;
906
+ exports.clampTimeout = clampTimeout;
826
907
  exports.persistFixture = persistFixture;
827
908
  exports.proxyAndRecord = proxyAndRecord;
909
+ exports.sanitizeHeaderValue = sanitizeHeaderValue;
828
910
  //# sourceMappingURL=recorder.cjs.map