@copilotkit/aimock 1.42.1 → 1.43.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 (338) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +50 -0
  4. package/README.md +102 -16
  5. package/dist/agui-mock.cjs +1 -1
  6. package/dist/agui-mock.cjs.map +1 -1
  7. package/dist/agui-mock.d.cts.map +1 -1
  8. package/dist/agui-mock.d.ts.map +1 -1
  9. package/dist/agui-mock.js +1 -1
  10. package/dist/agui-mock.js.map +1 -1
  11. package/dist/agui-recorder.cjs +33 -11
  12. package/dist/agui-recorder.cjs.map +1 -1
  13. package/dist/agui-recorder.d.cts +10 -1
  14. package/dist/agui-recorder.d.cts.map +1 -1
  15. package/dist/agui-recorder.d.ts +10 -1
  16. package/dist/agui-recorder.d.ts.map +1 -1
  17. package/dist/agui-recorder.js +33 -11
  18. package/dist/agui-recorder.js.map +1 -1
  19. package/dist/agui-types.d.cts +1 -0
  20. package/dist/agui-types.d.cts.map +1 -1
  21. package/dist/agui-types.d.ts +1 -0
  22. package/dist/agui-types.d.ts.map +1 -1
  23. package/dist/aimock-cli.cjs +38 -10
  24. package/dist/aimock-cli.cjs.map +1 -1
  25. package/dist/aimock-cli.d.cts +5 -0
  26. package/dist/aimock-cli.d.cts.map +1 -1
  27. package/dist/aimock-cli.d.ts +5 -0
  28. package/dist/aimock-cli.d.ts.map +1 -1
  29. package/dist/aimock-cli.js +38 -10
  30. package/dist/aimock-cli.js.map +1 -1
  31. package/dist/batches.cjs +458 -0
  32. package/dist/batches.cjs.map +1 -0
  33. package/dist/batches.d.cts +57 -0
  34. package/dist/batches.d.cts.map +1 -0
  35. package/dist/batches.d.ts +57 -0
  36. package/dist/batches.d.ts.map +1 -0
  37. package/dist/batches.js +453 -0
  38. package/dist/batches.js.map +1 -0
  39. package/dist/bedrock-converse.cjs +12 -8
  40. package/dist/bedrock-converse.cjs.map +1 -1
  41. package/dist/bedrock-converse.d.cts.map +1 -1
  42. package/dist/bedrock-converse.d.ts.map +1 -1
  43. package/dist/bedrock-converse.js +14 -10
  44. package/dist/bedrock-converse.js.map +1 -1
  45. package/dist/bedrock.cjs +10 -8
  46. package/dist/bedrock.cjs.map +1 -1
  47. package/dist/bedrock.d.cts.map +1 -1
  48. package/dist/bedrock.d.ts.map +1 -1
  49. package/dist/bedrock.js +12 -10
  50. package/dist/bedrock.js.map +1 -1
  51. package/dist/byteplus-video.cjs +3 -3
  52. package/dist/byteplus-video.cjs.map +1 -1
  53. package/dist/byteplus-video.js +4 -4
  54. package/dist/byteplus-video.js.map +1 -1
  55. package/dist/chaos.cjs +622 -72
  56. package/dist/chaos.cjs.map +1 -1
  57. package/dist/chaos.d.cts +164 -7
  58. package/dist/chaos.d.cts.map +1 -1
  59. package/dist/chaos.d.ts +164 -7
  60. package/dist/chaos.d.ts.map +1 -1
  61. package/dist/chaos.js +613 -74
  62. package/dist/chaos.js.map +1 -1
  63. package/dist/cli.cjs +42 -23
  64. package/dist/cli.cjs.map +1 -1
  65. package/dist/cli.js +42 -23
  66. package/dist/cli.js.map +1 -1
  67. package/dist/cohere.cjs +10 -8
  68. package/dist/cohere.cjs.map +1 -1
  69. package/dist/cohere.d.cts.map +1 -1
  70. package/dist/cohere.d.ts.map +1 -1
  71. package/dist/cohere.js +12 -10
  72. package/dist/cohere.js.map +1 -1
  73. package/dist/config-loader.d.ts.map +1 -1
  74. package/dist/convert.cjs +1 -1
  75. package/dist/convert.cjs.map +1 -1
  76. package/dist/convert.js +1 -1
  77. package/dist/convert.js.map +1 -1
  78. package/dist/elevenlabs-audio.cjs +3 -3
  79. package/dist/elevenlabs-audio.cjs.map +1 -1
  80. package/dist/elevenlabs-audio.js +4 -4
  81. package/dist/elevenlabs-audio.js.map +1 -1
  82. package/dist/elevenlabs-voice.cjs +1053 -0
  83. package/dist/elevenlabs-voice.cjs.map +1 -0
  84. package/dist/elevenlabs-voice.d.cts +72 -0
  85. package/dist/elevenlabs-voice.d.cts.map +1 -0
  86. package/dist/elevenlabs-voice.d.ts +72 -0
  87. package/dist/elevenlabs-voice.d.ts.map +1 -0
  88. package/dist/elevenlabs-voice.js +1048 -0
  89. package/dist/elevenlabs-voice.js.map +1 -0
  90. package/dist/embeddings.cjs +8 -4
  91. package/dist/embeddings.cjs.map +1 -1
  92. package/dist/embeddings.d.cts.map +1 -1
  93. package/dist/embeddings.d.ts.map +1 -1
  94. package/dist/embeddings.js +9 -5
  95. package/dist/embeddings.js.map +1 -1
  96. package/dist/fal-audio.cjs +3 -3
  97. package/dist/fal-audio.cjs.map +1 -1
  98. package/dist/fal-audio.js +4 -4
  99. package/dist/fal-audio.js.map +1 -1
  100. package/dist/fal.cjs +28 -4
  101. package/dist/fal.cjs.map +1 -1
  102. package/dist/fal.d.cts +16 -1
  103. package/dist/fal.d.cts.map +1 -1
  104. package/dist/fal.d.ts +16 -1
  105. package/dist/fal.d.ts.map +1 -1
  106. package/dist/fal.js +28 -5
  107. package/dist/fal.js.map +1 -1
  108. package/dist/files.cjs +1071 -0
  109. package/dist/files.cjs.map +1 -0
  110. package/dist/files.d.cts +68 -0
  111. package/dist/files.d.cts.map +1 -0
  112. package/dist/files.d.ts +68 -0
  113. package/dist/files.d.ts.map +1 -0
  114. package/dist/files.js +1059 -0
  115. package/dist/files.js.map +1 -0
  116. package/dist/fine-tuning.cjs +1051 -0
  117. package/dist/fine-tuning.cjs.map +1 -0
  118. package/dist/fine-tuning.d.cts +96 -0
  119. package/dist/fine-tuning.d.cts.map +1 -0
  120. package/dist/fine-tuning.d.ts +96 -0
  121. package/dist/fine-tuning.d.ts.map +1 -0
  122. package/dist/fine-tuning.js +1044 -0
  123. package/dist/fine-tuning.js.map +1 -0
  124. package/dist/fixture-loader.cjs +0 -0
  125. package/dist/fixture-loader.cjs.map +1 -1
  126. package/dist/fixture-loader.d.cts +37 -5
  127. package/dist/fixture-loader.d.cts.map +1 -1
  128. package/dist/fixture-loader.d.ts +37 -5
  129. package/dist/fixture-loader.d.ts.map +1 -1
  130. package/dist/fixture-loader.js +0 -0
  131. package/dist/fixture-loader.js.map +1 -1
  132. package/dist/gemini-embeddings.cjs +2 -2
  133. package/dist/gemini-embeddings.cjs.map +1 -1
  134. package/dist/gemini-embeddings.js +3 -3
  135. package/dist/gemini-embeddings.js.map +1 -1
  136. package/dist/gemini-interactions.cjs +13 -7
  137. package/dist/gemini-interactions.cjs.map +1 -1
  138. package/dist/gemini-interactions.d.cts.map +1 -1
  139. package/dist/gemini-interactions.d.ts.map +1 -1
  140. package/dist/gemini-interactions.js +15 -9
  141. package/dist/gemini-interactions.js.map +1 -1
  142. package/dist/gemini.cjs +11 -7
  143. package/dist/gemini.cjs.map +1 -1
  144. package/dist/gemini.d.cts.map +1 -1
  145. package/dist/gemini.d.ts.map +1 -1
  146. package/dist/gemini.js +13 -9
  147. package/dist/gemini.js.map +1 -1
  148. package/dist/grok-video.cjs +14 -14
  149. package/dist/grok-video.cjs.map +1 -1
  150. package/dist/grok-video.js +15 -15
  151. package/dist/grok-video.js.map +1 -1
  152. package/dist/helpers.cjs +221 -5
  153. package/dist/helpers.cjs.map +1 -1
  154. package/dist/helpers.d.cts +38 -2
  155. package/dist/helpers.d.cts.map +1 -1
  156. package/dist/helpers.d.ts +38 -2
  157. package/dist/helpers.d.ts.map +1 -1
  158. package/dist/helpers.js +213 -6
  159. package/dist/helpers.js.map +1 -1
  160. package/dist/images.cjs +54 -123
  161. package/dist/images.cjs.map +1 -1
  162. package/dist/images.d.cts.map +1 -1
  163. package/dist/images.d.ts.map +1 -1
  164. package/dist/images.js +55 -124
  165. package/dist/images.js.map +1 -1
  166. package/dist/index.cjs +41 -8
  167. package/dist/index.d.cts +12 -7
  168. package/dist/index.d.ts +12 -7
  169. package/dist/index.js +14 -10
  170. package/dist/jest.cjs +14 -7
  171. package/dist/jest.cjs.map +1 -1
  172. package/dist/jest.d.cts +3 -2
  173. package/dist/jest.d.cts.map +1 -1
  174. package/dist/jest.d.ts +3 -2
  175. package/dist/jest.d.ts.map +1 -1
  176. package/dist/jest.js +14 -7
  177. package/dist/jest.js.map +1 -1
  178. package/dist/journal.cjs +41 -4
  179. package/dist/journal.cjs.map +1 -1
  180. package/dist/journal.d.cts +44 -3
  181. package/dist/journal.d.cts.map +1 -1
  182. package/dist/journal.d.ts +44 -3
  183. package/dist/journal.d.ts.map +1 -1
  184. package/dist/journal.js +41 -5
  185. package/dist/journal.js.map +1 -1
  186. package/dist/live-fixture.cjs +1192 -0
  187. package/dist/live-fixture.cjs.map +1 -0
  188. package/dist/live-fixture.js +1187 -0
  189. package/dist/live-fixture.js.map +1 -0
  190. package/dist/live-recorder.cjs +256 -0
  191. package/dist/live-recorder.cjs.map +1 -0
  192. package/dist/live-recorder.js +254 -0
  193. package/dist/live-recorder.js.map +1 -0
  194. package/dist/live-replay.cjs +286 -0
  195. package/dist/live-replay.cjs.map +1 -0
  196. package/dist/live-replay.js +285 -0
  197. package/dist/live-replay.js.map +1 -0
  198. package/dist/live-sanitize.cjs +196 -0
  199. package/dist/live-sanitize.cjs.map +1 -0
  200. package/dist/live-sanitize.js +196 -0
  201. package/dist/live-sanitize.js.map +1 -0
  202. package/dist/live-types.d.cts +71 -0
  203. package/dist/live-types.d.cts.map +1 -0
  204. package/dist/live-types.d.ts +71 -0
  205. package/dist/live-types.d.ts.map +1 -0
  206. package/dist/llmock.cjs +52 -41
  207. package/dist/llmock.cjs.map +1 -1
  208. package/dist/llmock.d.cts +16 -5
  209. package/dist/llmock.d.cts.map +1 -1
  210. package/dist/llmock.d.ts +16 -5
  211. package/dist/llmock.d.ts.map +1 -1
  212. package/dist/llmock.js +53 -42
  213. package/dist/llmock.js.map +1 -1
  214. package/dist/messages.cjs +11 -7
  215. package/dist/messages.cjs.map +1 -1
  216. package/dist/messages.d.cts.map +1 -1
  217. package/dist/messages.d.ts.map +1 -1
  218. package/dist/messages.js +13 -9
  219. package/dist/messages.js.map +1 -1
  220. package/dist/metrics.cjs +258 -5
  221. package/dist/metrics.cjs.map +1 -1
  222. package/dist/metrics.d.cts +3 -1
  223. package/dist/metrics.d.cts.map +1 -1
  224. package/dist/metrics.d.ts +3 -1
  225. package/dist/metrics.d.ts.map +1 -1
  226. package/dist/metrics.js +255 -6
  227. package/dist/metrics.js.map +1 -1
  228. package/dist/moderation.cjs +10 -1
  229. package/dist/moderation.cjs.map +1 -1
  230. package/dist/moderation.d.cts.map +1 -1
  231. package/dist/moderation.d.ts.map +1 -1
  232. package/dist/moderation.js +10 -1
  233. package/dist/moderation.js.map +1 -1
  234. package/dist/ollama.cjs +5 -5
  235. package/dist/ollama.cjs.map +1 -1
  236. package/dist/ollama.js +6 -6
  237. package/dist/ollama.js.map +1 -1
  238. package/dist/openrouter-video.cjs +5 -5
  239. package/dist/openrouter-video.cjs.map +1 -1
  240. package/dist/openrouter-video.js +6 -6
  241. package/dist/openrouter-video.js.map +1 -1
  242. package/dist/recorder.cjs +45 -9
  243. package/dist/recorder.cjs.map +1 -1
  244. package/dist/recorder.d.cts.map +1 -1
  245. package/dist/recorder.d.ts.map +1 -1
  246. package/dist/recorder.js +43 -11
  247. package/dist/recorder.js.map +1 -1
  248. package/dist/responses.cjs +4 -3
  249. package/dist/responses.cjs.map +1 -1
  250. package/dist/responses.js +5 -5
  251. package/dist/responses.js.map +1 -1
  252. package/dist/router.cjs +20 -33
  253. package/dist/router.cjs.map +1 -1
  254. package/dist/router.d.cts +4 -1
  255. package/dist/router.d.cts.map +1 -1
  256. package/dist/router.d.ts +4 -1
  257. package/dist/router.d.ts.map +1 -1
  258. package/dist/router.js +20 -34
  259. package/dist/router.js.map +1 -1
  260. package/dist/server.cjs +701 -625
  261. package/dist/server.cjs.map +1 -1
  262. package/dist/server.d.cts +1 -0
  263. package/dist/server.d.cts.map +1 -1
  264. package/dist/server.d.ts +1 -0
  265. package/dist/server.d.ts.map +1 -1
  266. package/dist/server.js +704 -628
  267. package/dist/server.js.map +1 -1
  268. package/dist/speech.cjs +2 -2
  269. package/dist/speech.cjs.map +1 -1
  270. package/dist/speech.js +3 -3
  271. package/dist/speech.js.map +1 -1
  272. package/dist/transcription.cjs +4 -4
  273. package/dist/transcription.cjs.map +1 -1
  274. package/dist/transcription.js +5 -5
  275. package/dist/transcription.js.map +1 -1
  276. package/dist/types.d.cts +119 -16
  277. package/dist/types.d.cts.map +1 -1
  278. package/dist/types.d.ts +119 -16
  279. package/dist/types.d.ts.map +1 -1
  280. package/dist/validate-cli.cjs +925 -0
  281. package/dist/validate-cli.cjs.map +1 -0
  282. package/dist/validate-cli.js +924 -0
  283. package/dist/validate-cli.js.map +1 -0
  284. package/dist/veo-video.cjs +13 -13
  285. package/dist/veo-video.cjs.map +1 -1
  286. package/dist/veo-video.js +14 -14
  287. package/dist/veo-video.js.map +1 -1
  288. package/dist/video.cjs +4 -4
  289. package/dist/video.cjs.map +1 -1
  290. package/dist/video.d.cts +1 -1
  291. package/dist/video.d.cts.map +1 -1
  292. package/dist/video.d.ts +1 -1
  293. package/dist/video.d.ts.map +1 -1
  294. package/dist/video.js +5 -5
  295. package/dist/video.js.map +1 -1
  296. package/dist/vitest.cjs +13 -6
  297. package/dist/vitest.cjs.map +1 -1
  298. package/dist/vitest.d.cts +2 -1
  299. package/dist/vitest.d.cts.map +1 -1
  300. package/dist/vitest.d.ts +2 -1
  301. package/dist/vitest.d.ts.map +1 -1
  302. package/dist/vitest.js +14 -7
  303. package/dist/vitest.js.map +1 -1
  304. package/dist/watcher.cjs +2 -1
  305. package/dist/watcher.cjs.map +1 -1
  306. package/dist/watcher.js +2 -1
  307. package/dist/watcher.js.map +1 -1
  308. package/dist/ws-framing.cjs +150 -35
  309. package/dist/ws-framing.cjs.map +1 -1
  310. package/dist/ws-framing.d.cts +18 -3
  311. package/dist/ws-framing.d.cts.map +1 -1
  312. package/dist/ws-framing.d.ts +18 -3
  313. package/dist/ws-framing.d.ts.map +1 -1
  314. package/dist/ws-framing.js +151 -36
  315. package/dist/ws-framing.js.map +1 -1
  316. package/dist/ws-gemini-live.cjs +2 -2
  317. package/dist/ws-gemini-live.d.ts +2 -2
  318. package/dist/ws-gemini-live.js +2 -2
  319. package/dist/ws-live.cjs +401 -0
  320. package/dist/ws-live.cjs.map +1 -0
  321. package/dist/ws-live.js +400 -0
  322. package/dist/ws-live.js.map +1 -0
  323. package/dist/ws-realtime.cjs +46 -2
  324. package/dist/ws-realtime.cjs.map +1 -1
  325. package/dist/ws-realtime.d.cts.map +1 -1
  326. package/dist/ws-realtime.d.ts +2 -2
  327. package/dist/ws-realtime.d.ts.map +1 -1
  328. package/dist/ws-realtime.js +46 -2
  329. package/dist/ws-realtime.js.map +1 -1
  330. package/dist/ws-responses.cjs +2 -2
  331. package/dist/ws-responses.js +2 -2
  332. package/dist/ws-upstream.cjs +118 -0
  333. package/dist/ws-upstream.cjs.map +1 -0
  334. package/dist/ws-upstream.js +115 -0
  335. package/dist/ws-upstream.js.map +1 -0
  336. package/fixtures/openai-live-client.json +390 -0
  337. package/fixtures/openai-live-managed.json +709 -0
  338. package/package.json +1 -1
package/dist/server.js CHANGED
@@ -1,15 +1,16 @@
1
1
  import { DEFAULT_TEST_ID } from "./constants.js";
2
2
  import { markAuthenticatedRequest, resolveInboundAuth, validateRequestApiKey, writeApiKeyHttpRejection, writeApiKeyUpgradeRejection } from "./api-key-auth.js";
3
- import { buildContentWithToolCallsChunks, buildContentWithToolCallsCompletion, buildTextChunks, buildTextCompletion, buildToolCallChunks, buildToolCallCompletion, buildUsageChunk, extractOverrides, flattenHeaders, getContext, getTestId, isAudioResponse, isContentWithToolCallsResponse, isEmbeddingResponse, isErrorResponse, isImageResponse, isJSONResponse, isJsonObject, isResponseFactory, isTextResponse, isToolCallResponse, isTranscriptionResponse, isVideoResponse, readBody, resolveFixtureBlocks, resolveReasoningForModel, resolveResponse, resolveStrictMode, resolveUsage, serializeErrorResponse, strictNoMatchLogLine, strictNoMatchMessage, strictOverrideField } from "./helpers.js";
3
+ import { RequestBodyTooLargeError, buildContentWithToolCallsChunks, buildContentWithToolCallsCompletion, buildTextChunks, buildTextCompletion, buildToolCallChunks, buildToolCallCompletion, buildUsageChunk, describeMatch, extractOverrides, flattenHeaders, getContext, getTestId, isAudioResponse, isContentWithToolCallsResponse, isEmbeddingResponse, isErrorResponse, isImageResponse, isJSONResponse, isJsonObject, isResponseFactory, isTextResponse, isToolCallResponse, isTranscriptionResponse, isVideoResponse, markMintedRequestId, readBody, readBodyBufferBounded, resolveFixtureBlockOutcome, resolveReasoningForModel, resolveRequestId, resolveResponse, resolveStrictMode, resolveTestId, resolveUsage, serializeErrorResponse, strictNoMatchLogLine, strictNoMatchMessage, strictOverrideField, wouldProxyMiss } from "./helpers.js";
4
+ import { normalizeLiveOptions } from "./live-fixture.js";
5
+ import { writeErrorResponse, writeSSEStream } from "./sse-writer.js";
6
+ import { CHAOS_FIELDS, CHAOS_FIELD_NAMES, applyChaosAction, awaitChaosLatency, describeUnwritableReason, evaluateChaos, isChaosScope, parseChaosField, resetChaosWarnings, resolveChaosConfig, responseGoneReason } from "./chaos.js";
7
+ import { INJECTED_STATUS_RANGE, claimOneShotError, clearFixtureQueue, entryToFixture, isInjectableStatus, isOneShotError, queueOneShotError, releaseOneShotError, validateFixtures } from "./fixture-loader.js";
8
+ import { matchFixtureDiagnostic, recordMatchOptions } from "./router.js";
9
+ import { proxyAndRecord } from "./recorder.js";
4
10
  import { Logger } from "./logger.js";
5
11
  import { Journal } from "./journal.js";
6
- import { matchFixtureDiagnostic, recordMatchOptions } from "./router.js";
7
- import { INJECTED_STATUS_RANGE, entryToFixture, isInjectableStatus, validateFixtures } from "./fixture-loader.js";
8
- import { writeErrorResponse, writeSSEStream } from "./sse-writer.js";
9
12
  import { createInterruptionSignal } from "./interruption.js";
10
13
  import { buildOpenRouterCandidates, handleOpenRouterCredits, handleOpenRouterKey, handleOpenRouterModels, isOpenRouterPath, resolveOpenRouterShaping, serializeOpenRouterError, shapeOpenRouterChunks, shapeOpenRouterCompletion } from "./openrouter-chat.js";
11
- import { applyChaosAction, evaluateChaos, isChaosScope } from "./chaos.js";
12
- import { proxyAndRecord } from "./recorder.js";
13
14
  import { handleResponses } from "./responses.js";
14
15
  import { handleMessages } from "./messages.js";
15
16
  import { handleGemini } from "./gemini.js";
@@ -23,27 +24,35 @@ import { handleImageEdit, handleImageVariations, handleImages } from "./images.j
23
24
  import { handleSpeech } from "./speech.js";
24
25
  import { VideoStateMap, handleVideoCreate } from "./video.js";
25
26
  import { falJobs, handleFalQueue } from "./fal-audio.js";
26
- import { falQueueStates, handleFal } from "./fal.js";
27
+ import { falQueueStates, falWillHandle, handleFal } from "./fal.js";
27
28
  import { OPENROUTER_VIDEO_DEFAULT_MAX_CONTENT_BYTES, OpenRouterVideoJobMap, handleOpenRouterVideoContent, handleOpenRouterVideoCreate, handleOpenRouterVideoModels, handleOpenRouterVideoStatus } from "./openrouter-video.js";
28
29
  import { VeoVideoJobMap, handleVeoVideoCreate, handleVeoVideoStatus } from "./veo-video.js";
29
30
  import { GrokVideoJobMap, handleGrokVideoCreate, handleGrokVideoStatus } from "./grok-video.js";
30
31
  import { BytePlusVideoJobMap, handleBytePlusVideoCreate, handleBytePlusVideoStatus } from "./byteplus-video.js";
31
32
  import { handleElevenLabsAudio, handleElevenLabsTTS } from "./elevenlabs-audio.js";
33
+ import { clearElevenLabsVoices, handleElevenLabsVoiceCreate, handleElevenLabsVoiceDelete, handleElevenLabsVoiceDesign, handleElevenLabsVoiceGet } from "./elevenlabs-voice.js";
32
34
  import { handleOllama, handleOllamaEmbeddings, handleOllamaGenerate } from "./ollama.js";
33
35
  import { handleCohere, handleCohereEmbed } from "./cohere.js";
34
36
  import { handleSearch } from "./search.js";
35
37
  import { handleRerank } from "./rerank.js";
36
38
  import { handleModeration } from "./moderation.js";
39
+ import { clearFineTuningStore, handleFineTuningCancel, handleFineTuningCreate, handleFineTuningEvents, handleFineTuningList, handleFineTuningRetrieve } from "./fine-tuning.js";
40
+ import { FILES_BODY_DRAIN_MAX_BYTES, FILES_BODY_MAX_BYTES, FILES_BODY_OVERSIZED, clearFileStore, handleFilesContent, handleFilesCreate, handleFilesDelete, handleFilesList, handleFilesRetrieve } from "./files.js";
41
+ import { clearBatchStore, handleBatchesCancel, handleBatchesCreate, handleBatchesList, handleBatchesRetrieve } from "./batches.js";
37
42
  import { upgradeToWebSocket } from "./ws-framing.js";
43
+ import { handleLiveSession } from "./ws-live.js";
38
44
  import { handleWebSocketResponses } from "./ws-responses.js";
39
45
  import { handleWebSocketRealtime } from "./ws-realtime.js";
40
46
  import { handleWebSocketGeminiLive } from "./ws-gemini-live.js";
41
- import { BYTEPLUS_VIDEO_STATUS_RE, BYTEPLUS_VIDEO_SUBMIT_RE, GROK_VIDEO_STATUS_RE, GROK_VIDEO_SUBMIT_PATH, OPENROUTER_VIDEO_CONTENT_RE, OPENROUTER_VIDEO_STATUS_RE, VEO_OPERATION_RE, VEO_PREDICT_LRO_RE, createMetricsRegistry, normalizePathLabel } from "./metrics.js";
47
+ import { BYTEPLUS_VIDEO_STATUS_RE, BYTEPLUS_VIDEO_SUBMIT_RE, DESTROYED_STATUS_LABEL, FAL_ROUTE_RE, FILES_CONTENT_RE, FILES_ID_RE, GROK_VIDEO_STATUS_RE, GROK_VIDEO_SUBMIT_PATH, OPENROUTER_VIDEO_CONTENT_RE, OPENROUTER_VIDEO_STATUS_RE, VEO_OPERATION_RE, VEO_PREDICT_LRO_RE, createMetricsRegistry, normalizePathLabel } from "./metrics.js";
42
48
  import * as http$1 from "node:http";
49
+ import { AsyncLocalStorage } from "node:async_hooks";
43
50
 
44
51
  //#region src/server.ts
52
+ const liveClosers = /* @__PURE__ */ new WeakMap();
45
53
  const COMPLETIONS_PATH = "/v1/chat/completions";
46
54
  const RESPONSES_PATH = "/v1/responses";
55
+ const LIVE_PATH = "/v1/live/sessions";
47
56
  const REALTIME_PATH = "/v1/realtime";
48
57
  const GEMINI_LIVE_PATH = "/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent";
49
58
  const MESSAGES_PATH = "/v1/messages";
@@ -64,10 +73,12 @@ const GEMINI_PREDICT_RE = /^\/v1beta\/models\/([^:]+):predict$/;
64
73
  const ELEVENLABS_SOUND_GENERATION_PATH = "/v1/sound-generation";
65
74
  const ELEVENLABS_TTS_RE = /^\/v1\/text-to-speech\/([^/]+)$/;
66
75
  const ELEVENLABS_MUSIC_RE = /^\/v1\/music(?:\/(.+))?$/;
76
+ const ELEVENLABS_VOICE_DESIGN_PATH = "/v1/text-to-voice/design";
77
+ const ELEVENLABS_VOICE_CREATE_PATH = "/v1/text-to-voice";
78
+ const ELEVENLABS_VOICE_RE = /^\/v1\/voices\/([^/]+)$/;
67
79
  const FAL_QUEUE_SUBMIT_RE = /^\/fal\/queue\/submit\/(.+)$/;
68
80
  const FAL_QUEUE_REQUESTS_RE = /^\/fal\/queue\/requests\/(.+)$/;
69
81
  const FAL_RUN_RE = /^\/fal\/run\/(.+)$/;
70
- const FAL_PREFIX_RE = /^\/fal(?:\/.*)?$/;
71
82
  const DEFAULT_CHUNK_SIZE = 20;
72
83
  const COMPAT_SUFFIXES = [
73
84
  "/chat/completions",
@@ -116,6 +127,14 @@ const HEALTH_PATH = "/health";
116
127
  const READY_PATH = "/ready";
117
128
  const MODELS_PATH = "/v1/models";
118
129
  const REQUESTS_PATH = "/v1/_requests";
130
+ const BATCHES_PATH = "/v1/batches";
131
+ const BATCHES_ID_RE = /^\/v1\/batches\/([^/]+)$/;
132
+ const BATCHES_CANCEL_RE = /^\/v1\/batches\/([^/]+)\/cancel$/;
133
+ const FILES_PATH = "/v1/files";
134
+ const FINE_TUNING_JOBS_PATH = "/v1/fine_tuning/jobs";
135
+ const FINE_TUNING_ID_RE = /^\/v1\/fine_tuning\/jobs\/([^/]+)$/;
136
+ const FINE_TUNING_CANCEL_RE = /^\/v1\/fine_tuning\/jobs\/([^/]+)\/cancel$/;
137
+ const FINE_TUNING_EVENTS_RE = /^\/v1\/fine_tuning\/jobs\/([^/]+)\/events$/;
119
138
  const DEFAULT_MODELS = [
120
139
  "gpt-4",
121
140
  "gpt-4o",
@@ -127,7 +146,7 @@ const CORS_HEADERS = {
127
146
  "Access-Control-Allow-Origin": "*",
128
147
  "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
129
148
  "Access-Control-Allow-Headers": "*",
130
- "Access-Control-Expose-Headers": "X-Total-Count"
149
+ "Access-Control-Expose-Headers": "X-Total-Count, X-Request-Id"
131
150
  };
132
151
  function setCorsHeaders(res) {
133
152
  for (const [key, value] of Object.entries(CORS_HEADERS)) res.setHeader(key, value);
@@ -155,14 +174,18 @@ const JOURNAL_PARAMS = new Set([
155
174
  "method",
156
175
  "status",
157
176
  "service",
158
- "testId"
177
+ "testId",
178
+ "requestId"
159
179
  ]);
160
180
  /**
161
181
  * Perform a full reset: clear the fixtures array, the journal (entries *and*
162
182
  * per-test fixture match-counts, i.e. sequence position), the video and fal.ai
163
- * job/queue state, the Gemini interaction/event-id counters, and any runtime
164
- * chaos override (reverting to the construction-time chaos config), then
165
- * re-zero the `aimock_fixtures_loaded` gauge.
183
+ * job/queue state, the fine-tuning store (jobs, their append-only event logs,
184
+ * their poll counts, the create-time suffixes their model names are built from,
185
+ * and the module's monotonic clock), the ElevenLabs Voice Design store, the
186
+ * Gemini interaction/event-id counters, and any runtime chaos override
187
+ * (reverting to the construction-time chaos config), then re-zero the
188
+ * `aimock_fixtures_loaded` gauge.
166
189
  *
167
190
  * `targets` is `null` when no server is running (an in-process `reset()` before
168
191
  * `start()`). The process-global generation state is reset either way, since it
@@ -172,13 +195,19 @@ const JOURNAL_PARAMS = new Set([
172
195
  * `POST /__aimock/reset/fixtures` alias, and `LLMock.reset()`.
173
196
  */
174
197
  function performFullReset(fixtures, targets) {
175
- fixtures.length = 0;
198
+ clearFixtureQueue(fixtures);
176
199
  falJobs.clear();
177
200
  falQueueStates.clear();
201
+ clearBatchStore();
202
+ clearElevenLabsVoices();
203
+ clearFileStore();
204
+ clearFineTuningStore();
178
205
  resetInteractionCounter();
179
206
  resetEventIdCounter();
180
207
  if (!targets) return;
208
+ liveClosers.get(targets.defaults)?.();
181
209
  targets.journal.clear();
210
+ resetChaosWarnings(targets.defaults.logger);
182
211
  targets.defaults.chaos = void 0;
183
212
  targets.videoStates.clear();
184
213
  targets.openRouterVideoJobs.clear();
@@ -230,22 +259,18 @@ function fixtureResponseKind(response) {
230
259
  return "unknown";
231
260
  }
232
261
  /**
233
- * Resolve a journal entry's testId exactly the way `getTestId` resolves an
234
- * incoming request's: the `X-Test-Id` header wins, then `?testId=` parsed out
235
- * of the query string, then `DEFAULT_TEST_ID`. The
236
- * filter and the server's own per-test isolation must never disagree — and a
237
- * raw `path.includes("testId=t1")` both prefix-collides with `t10` and matches
238
- * unrelated params like `notTestId`.
262
+ * The testId a journal entry was scoped under — `resolveTestId` applied to the
263
+ * journaled headers and path, i.e. the SAME function that scoped the request
264
+ * (fixture match-counts, chaos) when it arrived. Node folds a repeated
265
+ * `X-Test-Id` into one string ("a, a") before either side sees it, and the
266
+ * request is counted and chaos-evaluated under that folded string, so the
267
+ * `?testId=` filter matches it verbatim too: no token-splitting here that the
268
+ * scoping path does not also do. (Splitting on "," additionally broke every
269
+ * legitimately comma-bearing id.) A raw `path.includes("testId=t1")` both
270
+ * prefix-collides with `t10` and matches unrelated params like `notTestId`.
239
271
  */
240
272
  function journalEntryTestId(entry) {
241
- const header = entry.headers["x-test-id"];
242
- if (header) return header;
243
- const qIdx = entry.path.indexOf("?");
244
- if (qIdx !== -1) {
245
- const queryValue = new URLSearchParams(entry.path.slice(qIdx + 1)).get("testId");
246
- if (queryValue) return queryValue;
247
- }
248
- return DEFAULT_TEST_ID;
273
+ return resolveTestId(entry.headers, entry.path);
249
274
  }
250
275
  /**
251
276
  * The testId a chaos override is stored under, and read back with — resolved by
@@ -272,10 +297,14 @@ function writeBlankTestId(res) {
272
297
  res.end(JSON.stringify({ error: "Invalid 'X-Test-Id': header is present but empty" }));
273
298
  return true;
274
299
  }
275
- /** The chaos config in effect for one testId: its override, else the baseline. */
300
+ /**
301
+ * The chaos config in effect for one testId: its override, else the baseline —
302
+ * one or the other, never the two merged. Mirrors `resolveScopedDefaults` in
303
+ * chaos.ts, so what `GET /__aimock/chaos` reports for a scope is exactly what
304
+ * that scope's traffic is evaluated against.
305
+ */
276
306
  function effectiveChaos(defaults, scopeId) {
277
- const scoped = defaults.chaosByTestId?.get(scopeId);
278
- if (scoped) return scoped;
307
+ if (defaults.chaosByTestId?.has(scopeId)) return defaults.chaosByTestId.get(scopeId) ?? {};
279
308
  const current = defaults.chaos;
280
309
  if (!current) return {};
281
310
  return isChaosScope(current) ? current.base ?? {} : current;
@@ -320,6 +349,7 @@ async function handleControlAPI(req, res, pathname, searchParams, fixtures, jour
320
349
  const methodFilter = searchParams.get("method");
321
350
  const serviceFilter = searchParams.get("service");
322
351
  const testIdFilter = searchParams.get("testId");
352
+ const requestIdFilter = searchParams.get("requestId");
323
353
  let entries = journal.getAll();
324
354
  if (methodFilter !== null) {
325
355
  const want = methodFilter.toUpperCase();
@@ -329,6 +359,7 @@ async function handleControlAPI(req, res, pathname, searchParams, fixtures, jour
329
359
  if (statusFilter !== void 0) entries = entries.filter((e) => e.response.status === statusFilter);
330
360
  if (serviceFilter !== null) entries = entries.filter((e) => e.service === serviceFilter);
331
361
  if (testIdFilter !== null) entries = entries.filter((e) => journalEntryTestId(e) === testIdFilter);
362
+ if (requestIdFilter !== null) entries = entries.filter((e) => e.headers["x-request-id"] === requestIdFilter);
332
363
  const total = entries.length;
333
364
  if (offset > 0) entries = entries.slice(offset);
334
365
  if (limit >= 0) entries = entries.slice(0, limit);
@@ -362,7 +393,7 @@ async function handleControlAPI(req, res, pathname, searchParams, fixtures, jour
362
393
  fixtures: fixtures.map((fixture, index) => ({
363
394
  index,
364
395
  match: redactForInspection(fixture.match),
365
- responseKind: fixtureResponseKind(fixture.response),
396
+ responseKind: isOneShotError(fixture) ? "error" : fixtureResponseKind(fixture.response),
366
397
  ...fixture.latency !== void 0 ? { latency: fixture.latency } : {},
367
398
  ...fixture.chaos !== void 0 ? { chaos: fixture.chaos } : {}
368
399
  }))
@@ -412,26 +443,29 @@ async function handleControlAPI(req, res, pathname, searchParams, fixtures, jour
412
443
  res.end(JSON.stringify({ error: "Invalid body: expected a JSON object" }));
413
444
  return true;
414
445
  }
415
- const allowed = [
416
- "dropRate",
417
- "malformedRate",
418
- "disconnectRate"
419
- ];
420
- for (const key of Object.keys(parsed)) if (!allowed.includes(key)) {
446
+ for (const key of Object.keys(parsed)) if (!CHAOS_FIELD_NAMES.includes(key)) {
421
447
  res.writeHead(400, { "Content-Type": "application/json" });
422
448
  res.end(JSON.stringify({ error: `Unknown chaos field: '${key}'` }));
423
449
  return true;
424
450
  }
425
451
  const next = {};
426
- for (const key of allowed) {
452
+ for (const key of CHAOS_FIELD_NAMES) {
427
453
  const value = parsed[key];
428
454
  if (value === void 0) continue;
429
- if (typeof value !== "number" || Number.isNaN(value) || value < 0 || value > 1) {
455
+ const accepted = typeof value === "number" ? parseChaosField(key, value) : void 0;
456
+ if (accepted === void 0) {
457
+ const shape = CHAOS_FIELDS[key].integer ? "a whole number of ms" : "a number";
430
458
  res.writeHead(400, { "Content-Type": "application/json" });
431
- res.end(JSON.stringify({ error: `Invalid '${key}': must be a number between 0 and 1` }));
459
+ res.end(JSON.stringify({ error: `Invalid '${key}': must be ${shape} between 0 and ${CHAOS_FIELDS[key].max}` }));
432
460
  return true;
433
461
  }
434
- next[key] = value;
462
+ next[key] = accepted;
463
+ }
464
+ const shadowed = effectiveChaos(defaults, scopeId);
465
+ const droppedFields = Object.keys(shadowed).filter((key) => shadowed[key] !== void 0 && next[key] === void 0);
466
+ if (droppedFields.length > 0) {
467
+ const lost = droppedFields.map((key) => `${key}=${shadowed[key]}`).join(", ");
468
+ defaults.logger.warn(`[chaos] POST /__aimock/chaos (testId '${scopeId}') replaces the chaos config for this scope wholesale, it is not merged: ${lost} no longer applies here. Restate the field in the body to keep it.`);
435
469
  }
436
470
  defaults.chaosByTestId?.set(scopeId, next);
437
471
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -481,7 +515,7 @@ async function handleControlAPI(req, res, pathname, searchParams, fixtures, jour
481
515
  return true;
482
516
  }
483
517
  if (subPath === "/fixtures" && req.method === "DELETE") {
484
- fixtures.length = 0;
518
+ clearFixtureQueue(fixtures);
485
519
  if (defaults.registry) defaults.registry.setGauge("aimock_fixtures_loaded", {}, fixtures.length);
486
520
  res.writeHead(200, { "Content-Type": "application/json" });
487
521
  res.end(JSON.stringify({ cleared: true }));
@@ -575,32 +609,7 @@ async function handleControlAPI(req, res, pathname, searchParams, fixtures, jour
575
609
  }
576
610
  }
577
611
  }
578
- const errorFixture = {
579
- match: { predicate: () => true },
580
- response: {
581
- error: {
582
- message: errorBody?.message ?? "Injected error",
583
- type: errorBody?.type ?? "server_error",
584
- code: errorBody?.code
585
- },
586
- status
587
- }
588
- };
589
- fixtures.unshift(errorFixture);
590
- let consumed = false;
591
- const original = errorFixture.match.predicate;
592
- errorFixture.match.predicate = (req) => {
593
- if (consumed) return false;
594
- const result = original(req);
595
- if (result) {
596
- consumed = true;
597
- queueMicrotask(() => {
598
- const idx = fixtures.indexOf(errorFixture);
599
- if (idx !== -1) fixtures.splice(idx, 1);
600
- });
601
- }
602
- return result;
603
- };
612
+ queueOneShotError(fixtures, status, errorBody ?? void 0);
604
613
  res.writeHead(200, { "Content-Type": "application/json" });
605
614
  res.end(JSON.stringify({ queued: true }));
606
615
  return true;
@@ -608,6 +617,19 @@ async function handleControlAPI(req, res, pathname, searchParams, fixtures, jour
608
617
  handleNotFound(res, `Unknown control endpoint: ${pathname}`);
609
618
  return true;
610
619
  }
620
+ /**
621
+ * The client tore the connection down before its request body had fully
622
+ * arrived. Node rejects the pending body read with `aborted` (ECONNRESET) and
623
+ * destroys the response, so the rejection surfaces in the handler's read arm
624
+ * looking like any other thrown error. It is caller behaviour, not a server
625
+ * fault: no status can reach the client, so none must be journaled as
626
+ * delivered. A body that blew its size cap also leaves the request destroyed
627
+ * and incomplete, but that one is aimock's own decision, not a peer abort.
628
+ */
629
+ function clientAbortedMidBody(req, res, err) {
630
+ if (err instanceof RequestBodyTooLargeError) return false;
631
+ return res.destroyed && !req.complete;
632
+ }
611
633
  async function handleCompletions(req, res, fixtures, journal, defaults, modelFallback, providerKey, openRouter = false) {
612
634
  setCorsHeaders(res);
613
635
  let raw;
@@ -615,19 +637,43 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
615
637
  raw = await readBody(req);
616
638
  } catch (err) {
617
639
  const msg = err instanceof Error ? err.message : "Failed to read request body";
640
+ const clientFault = err instanceof RequestBodyTooLargeError;
641
+ const route = `${req.method ?? "POST"} ${req.url ?? COMPLETIONS_PATH}`;
642
+ const clientAbort = clientAbortedMidBody(req, res, err);
643
+ const bodyLimitDisconnect = clientFault && (req.destroyed || res.destroyed);
644
+ if (clientAbort || bodyLimitDisconnect) {
645
+ const interruptReason = bodyLimitDisconnect ? "request body exceeded size limit" : "client aborted";
646
+ defaults.logger.warn(bodyLimitDisconnect ? `${route}: ${msg}` : `${route}: client aborted mid-body (${msg})`);
647
+ journal.add({
648
+ method: req.method ?? "POST",
649
+ path: req.url ?? COMPLETIONS_PATH,
650
+ headers: flattenHeaders(req.headers),
651
+ body: null,
652
+ response: {
653
+ status: 0,
654
+ fixture: null,
655
+ interrupted: true,
656
+ interruptReason
657
+ }
658
+ });
659
+ return;
660
+ }
661
+ const status = clientFault ? 400 : 500;
662
+ if (clientFault) defaults.logger.warn(`${route}: ${msg}`);
663
+ else defaults.logger.error(`${route}: ${msg}`);
618
664
  journal.add({
619
665
  method: req.method ?? "POST",
620
666
  path: req.url ?? COMPLETIONS_PATH,
621
667
  headers: flattenHeaders(req.headers),
622
668
  body: null,
623
669
  response: {
624
- status: 500,
670
+ status,
625
671
  fixture: null
626
672
  }
627
673
  });
628
- writeErrorResponse(res, 500, openRouter ? serializeOpenRouterError(500, `Request body read failed: ${msg}`) : JSON.stringify({ error: {
674
+ writeErrorResponse(res, status, openRouter ? serializeOpenRouterError(status, `Request body read failed: ${msg}`) : JSON.stringify({ error: {
629
675
  message: `Request body read failed: ${msg}`,
630
- type: "server_error"
676
+ type: clientFault ? "invalid_request_error" : "server_error"
631
677
  } }));
632
678
  return;
633
679
  }
@@ -703,7 +749,15 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
703
749
  let preResolvedResponse = null;
704
750
  let responseModel = body.model;
705
751
  let fixtureCountIncremented = false;
706
- if (openRouter && Array.isArray(body.models) && body.models.length > 0) {
752
+ const openRouterFallback = openRouter && Array.isArray(body.models) && body.models.length > 0;
753
+ const selectFixture = (probe, counts) => {
754
+ for (;;) {
755
+ const attempt = matchFixtureDiagnostic(fixtures, probe, counts, defaults.requestTransform, matchOptions);
756
+ if (attempt.fixture && isOneShotError(attempt.fixture) && !claimOneShotError(fixtures, attempt.fixture)) continue;
757
+ return attempt;
758
+ }
759
+ };
760
+ if (openRouterFallback) {
707
761
  const candidates = buildOpenRouterCandidates(body);
708
762
  let matchCounts = journal.getFixtureMatchCountsForTest(testId);
709
763
  const allowFallbacks = body.provider?.allow_fallbacks !== false;
@@ -714,7 +768,7 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
714
768
  ...body,
715
769
  model: candidate
716
770
  };
717
- const attempt = matchFixtureDiagnostic(fixtures, probe, matchCounts, defaults.requestTransform, matchOptions);
771
+ const attempt = selectFixture(probe, matchCounts);
718
772
  skippedBySequenceOrTurn = Math.max(skippedBySequenceOrTurn, attempt.skippedBySequenceOrTurn);
719
773
  if (!attempt.fixture) return null;
720
774
  const response = await resolveResponse(attempt.fixture, probe);
@@ -751,35 +805,48 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
751
805
  preResolvedResponse = lastErrorResponse;
752
806
  }
753
807
  } else {
754
- const single = matchFixtureDiagnostic(fixtures, body, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform, matchOptions);
808
+ const single = selectFixture(body, journal.getFixtureMatchCountsForTest(testId));
755
809
  fixture = single.fixture;
756
810
  skippedBySequenceOrTurn = single.skippedBySequenceOrTurn;
757
811
  }
758
812
  if (fixture) {
759
813
  if (!fixtureCountIncremented) journal.incrementFixtureMatchCount(fixture, fixtures, testId);
760
- defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);
814
+ defaults.logger.debug(`Fixture matched: ${describeMatch(fixture.match, fixtures.indexOf(fixture))}`);
761
815
  } else {
762
816
  const lastUserMsg = body.messages.filter((m) => m.role === "user").pop();
763
817
  const snippet = typeof lastUserMsg?.content === "string" ? lastUserMsg.content.slice(0, 80) : "";
764
818
  defaults.logger.debug(`No fixture matched for request (model=${body.model ?? "?"}, msg="${snippet}")`);
765
819
  }
766
- const chaosAction = evaluateChaos(fixture, defaults.chaos, req.headers, defaults.logger, req.url);
820
+ const chaosConfig = resolveChaosConfig(fixture, defaults.chaos, req.headers, defaults.logger, req.url);
821
+ await awaitChaosLatency(fixture, defaults.chaos, req.headers, defaults.logger, req.url, res, chaosConfig);
822
+ const goneAfterLatency = responseGoneReason(res);
823
+ if (goneAfterLatency !== null) {
824
+ releaseOneShotError(fixtures, fixture);
825
+ defaults.logger.debug(`[chaos] ${method} ${path}: ${describeUnwritableReason(goneAfterLatency)} after the latency delay — not served, not journalled`);
826
+ return;
827
+ }
828
+ const chaosAction = evaluateChaos(fixture, defaults.chaos, req.headers, defaults.logger, req.url, chaosConfig);
767
829
  const chaosContext = {
768
830
  method,
769
831
  path,
770
832
  headers: flatHeaders,
771
833
  body
772
834
  };
773
- if (chaosAction === "drop" || chaosAction === "disconnect") {
774
- applyChaosAction(chaosAction, res, fixture, journal, chaosContext, fixture ? "fixture" : "proxy", defaults.registry);
835
+ const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);
836
+ const missWouldProxy = wouldProxyMiss(effectiveStrict, defaults.record, providerKey);
837
+ const noFixtureSource = missWouldProxy ? "proxy" : "internal";
838
+ if (chaosAction === "drop" || chaosAction === "disconnect" || chaosAction === "rateLimit") {
839
+ applyChaosAction(chaosAction, res, fixture, journal, chaosContext, fixture ? "fixture" : noFixtureSource, defaults.registry, defaults.logger);
840
+ releaseOneShotError(fixtures, fixture);
775
841
  return;
776
842
  }
777
- if (fixture && chaosAction === "malformed") {
778
- applyChaosAction(chaosAction, res, fixture, journal, chaosContext, "fixture", defaults.registry);
843
+ if (chaosAction === "malformed" && (fixture || !missWouldProxy)) {
844
+ applyChaosAction(chaosAction, res, fixture, journal, chaosContext, fixture ? "fixture" : "internal", defaults.registry, defaults.logger);
845
+ releaseOneShotError(fixtures, fixture);
779
846
  return;
780
847
  }
781
848
  if (!fixture) {
782
- if (resolveStrictMode(defaults.strict, req.headers)) {
849
+ if (effectiveStrict) {
783
850
  const strictStatus = 503;
784
851
  const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);
785
852
  defaults.logger.error(strictNoMatchLogLine(req.method ?? "POST", req.url ?? COMPLETIONS_PATH, skippedBySequenceOrTurn));
@@ -805,7 +872,7 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
805
872
  if (defaults.record && providerKey) {
806
873
  const hookOptions = chaosAction === "malformed" ? {
807
874
  beforeWriteResponse: () => {
808
- applyChaosAction(chaosAction, res, null, journal, chaosContext, "proxy", defaults.registry);
875
+ applyChaosAction(chaosAction, res, null, journal, chaosContext, "proxy", defaults.registry, defaults.logger);
809
876
  return true;
810
877
  },
811
878
  onHookBypassed: (reason) => {
@@ -817,9 +884,22 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
817
884
  });
818
885
  }
819
886
  } : void 0;
820
- const outcome = await proxyAndRecord(req, res, body, providerKey, req.url ?? COMPLETIONS_PATH, fixtures, defaults, raw, hookOptions);
887
+ let clientHungUp = false;
888
+ const noteClientHangUp = () => {
889
+ if (!res.writableEnded) clientHungUp = true;
890
+ };
891
+ req.socket?.on("end", noteClientHangUp);
892
+ req.socket?.on("error", noteClientHangUp);
893
+ let outcome;
894
+ try {
895
+ outcome = await proxyAndRecord(req, res, body, providerKey, req.url ?? COMPLETIONS_PATH, fixtures, defaults, raw, hookOptions);
896
+ } finally {
897
+ req.socket?.off("end", noteClientHangUp);
898
+ req.socket?.off("error", noteClientHangUp);
899
+ }
821
900
  if (outcome === "handled_by_hook") return;
822
901
  if (outcome !== "not_configured") {
902
+ const destroyedMidStream = res.destroyed && !res.writableEnded;
823
903
  journal.add({
824
904
  method: req.method ?? "POST",
825
905
  path: req.url ?? COMPLETIONS_PATH,
@@ -828,11 +908,19 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
828
908
  response: {
829
909
  status: res.statusCode ?? 200,
830
910
  fixture: null,
831
- source: "proxy"
911
+ source: "proxy",
912
+ ...destroyedMidStream ? {
913
+ interrupted: true,
914
+ interruptReason: clientHungUp ? "client aborted" : "proxy stream destroyed"
915
+ } : {}
832
916
  }
833
917
  });
834
918
  return;
835
919
  }
920
+ if (chaosAction === "malformed") {
921
+ applyChaosAction(chaosAction, res, null, journal, chaosContext, "internal", defaults.registry, defaults.logger);
922
+ return;
923
+ }
836
924
  }
837
925
  journal.add({
838
926
  method: req.method ?? "POST",
@@ -908,7 +996,7 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
908
996
  const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);
909
997
  const effReasoning = resolveReasoningForModel(response.reasoning, responseModel, effectiveStrict, defaults.logger);
910
998
  const streaming = body.stream === true;
911
- const streamingBlocks = streaming && response.blocks && response.blocks.length > 0 ? resolveFixtureBlocks(response.blocks) : void 0;
999
+ const blockOutcome = response.blocks && response.blocks.length > 0 ? resolveFixtureBlockOutcome(response.blocks) : void 0;
912
1000
  const journalEntry = journal.add({
913
1001
  method: req.method ?? "POST",
914
1002
  path: req.url ?? COMPLETIONS_PATH,
@@ -920,12 +1008,15 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
920
1008
  }
921
1009
  });
922
1010
  if (!streaming) {
923
- const completion = buildContentWithToolCallsCompletion(response.content ?? "", response.toolCalls ?? [], responseModel, effReasoning, overrides, body.messages);
1011
+ const completion = buildContentWithToolCallsCompletion(blockOutcome?.content ?? response.content ?? "", blockOutcome?.toolCalls ?? response.toolCalls ?? [], responseModel, effReasoning, blockOutcome ? {
1012
+ ...overrides,
1013
+ finishReason: overrides?.finishReason ?? (blockOutcome.hasToolCalls ? "tool_calls" : "stop")
1014
+ } : overrides, body.messages);
924
1015
  res.writeHead(200, { "Content-Type": "application/json" });
925
1016
  res.end(JSON.stringify(shapeORCompletion(completion, overrides)));
926
1017
  } else {
927
- const chunks = buildContentWithToolCallsChunks(response.content ?? "", response.toolCalls ?? [], responseModel, chunkSize, effReasoning, overrides, streamingBlocks);
928
- const completionText = streamingBlocks ? streamingBlocks.map((b) => b.type === "text" ? b.text : b.name + b.arguments).join("") : (response.content ?? "") + (response.toolCalls ?? []).map((tc) => tc.name + tc.arguments).join("");
1018
+ const chunks = buildContentWithToolCallsChunks(blockOutcome?.content ?? response.content ?? "", blockOutcome?.toolCalls ?? response.toolCalls ?? [], responseModel, chunkSize, effReasoning, overrides, blockOutcome?.ordered);
1019
+ const completionText = blockOutcome ? blockOutcome.content + blockOutcome.toolCalls.map((tc) => tc.name + tc.arguments).join("") : (response.content ?? "") + (response.toolCalls ?? []).map((tc) => tc.name + tc.arguments).join("");
929
1020
  const usageChunk = emitStreamingUsage ? buildUsageChunk(chunks[0]?.id ?? "chatcmpl-unknown", overrides?.model ?? responseModel, chunks[0]?.created ?? Math.floor(Date.now() / 1e3), resolveStreamingUsageTokens(overrides, completionText), overrides?.systemFingerprint) : void 0;
930
1021
  shapeORChunks(chunks, usageChunk, overrides);
931
1022
  const interruption = createInterruptionSignal(fixture);
@@ -1109,21 +1200,14 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1109
1200
  }
1110
1201
  };
1111
1202
  if (options?.chaos) {
1112
- const chaosRates = [
1113
- {
1114
- name: "dropRate",
1115
- value: options.chaos.dropRate
1116
- },
1117
- {
1118
- name: "malformedRate",
1119
- value: options.chaos.malformedRate
1120
- },
1121
- {
1122
- name: "disconnectRate",
1123
- value: options.chaos.disconnectRate
1124
- }
1125
- ];
1126
- for (const { name, value } of chaosRates) if (value !== void 0 && (value < 0 || value > 1)) logger.warn(`Chaos ${name} (${value}) is outside 0-1 range — will be clamped at runtime`);
1203
+ const chaosDefaults = options.chaos;
1204
+ for (const field of CHAOS_FIELD_NAMES) {
1205
+ const value = chaosDefaults[field];
1206
+ if (value === void 0) continue;
1207
+ if (typeof value === "number" && parseChaosField(field, value) !== void 0) continue;
1208
+ const shape = CHAOS_FIELDS[field].integer ? "a whole number of ms" : "a number";
1209
+ logger.warn(`Chaos default ${field} value ${JSON.stringify(value)} — must be ${shape} in [0,${CHAOS_FIELDS[field].max}]; rejected at runtime, never clamped: this default is ignored`);
1210
+ }
1127
1211
  }
1128
1212
  for (const { name, config } of [
1129
1213
  {
@@ -1169,62 +1253,241 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1169
1253
  if (basePath !== void 0 && /(^|\/)api\/v3\/?$/.test(basePath)) throw new Error(`record.providers.byteplus (${bytePlusBase}) ends in /api/v3, but aimock appends the /api/v3 prefix itself — every BytePlus request would go to /api/v3/api/v3/... and fail upstream. Configure the ORIGIN only (e.g. https://ark.ap-southeast.bytepluses.com).`);
1170
1254
  }
1171
1255
  }
1256
+ /**
1257
+ * The in-flight request, scoped to the async execution that serves it.
1258
+ * Entered once in the `http.createServer` callback, so every handler —
1259
+ * including the ones in other modules that only ever see the shared
1260
+ * `journal` — runs inside it.
1261
+ */
1262
+ const requestScope = new AsyncLocalStorage();
1263
+ /** The journal entry each request produced, keyed by the request object. */
1264
+ const ownJournalEntry = /* @__PURE__ */ new WeakMap();
1172
1265
  const journal = new Journal({
1173
1266
  maxEntries: options?.journalMaxEntries ?? 1e3,
1174
- fixtureCountsMaxTestIds: options?.fixtureCountsMaxTestIds ?? 500
1267
+ fixtureCountsMaxTestIds: options?.fixtureCountsMaxTestIds ?? 500,
1268
+ onAdd: (entry) => {
1269
+ const req = requestScope.getStore();
1270
+ if (req) ownJournalEntry.set(req, entry);
1271
+ }
1175
1272
  });
1176
1273
  const videoStates = new VideoStateMap();
1177
1274
  const openRouterVideoJobs = new OpenRouterVideoJobMap();
1178
1275
  const veoVideoJobs = new VeoVideoJobMap();
1179
1276
  const grokVideoJobs = new GrokVideoJobMap();
1180
1277
  const bytePlusVideoJobs = new BytePlusVideoJobMap();
1278
+ /**
1279
+ * The OpenAI error `type` that goes with a status: 4xx is the caller's
1280
+ * fault, everything else is ours. Shared by `routeError` and the envelopes
1281
+ * it calls so the body can never disagree with the status line.
1282
+ */
1283
+ function errorTypeForStatus(status) {
1284
+ return status >= 400 && status < 500 ? "invalid_request_error" : "server_error";
1285
+ }
1286
+ /**
1287
+ * The newest journal entry this request already produced, or null.
1288
+ *
1289
+ * Attribution is by the identity of the `IncomingMessage` the entry was
1290
+ * written under — never by `x-request-id`. That header is CALLER-supplied
1291
+ * (`resolveRequestId` echoes a well-formed one) and nothing makes it
1292
+ * unique: a retry, a harness that pins one id, or a load generator sends
1293
+ * the same id on many requests, and keying off it made a crash rewrite an
1294
+ * EARLIER request's entry while leaving the crashing request untraced. The
1295
+ * request object cannot be collided by a caller, so one entry belongs to
1296
+ * exactly one request. A handler writes at most one entry; if it writes
1297
+ * more, the newest wins, which is the entry the old scan would have found.
1298
+ */
1299
+ function lastJournalEntryFor(req) {
1300
+ return ownJournalEntry.get(req) ?? null;
1301
+ }
1302
+ /**
1303
+ * Uniform terminal arm for a route handler that threw. Every route error
1304
+ * path logs the route, journals the failure, sets CORS and answers the error
1305
+ * envelope — so a handler crash is never invisible in the logs or in
1306
+ * `/__aimock/journal`.
1307
+ *
1308
+ * ONE request produces ONE entry. `res.headersSent` is not the test for that:
1309
+ * a handler that journaled its 200 and then threw before `writeHead` has
1310
+ * sent no headers, and journaling again would leave two contradicting
1311
+ * entries for one request. So the existing entry is looked up by request
1312
+ * identity ({@link lastJournalEntryFor}) and AMENDED — status to what the
1313
+ * client actually got when the response is still changeable, or interrupted
1314
+ * when it isn't — and a fresh entry is
1315
+ * added only when the handler journaled nothing. That holds after the
1316
+ * response too: a handler that answered and then crashed before its own
1317
+ * journal write gets an entry carrying the status already sent, so a
1318
+ * post-response crash is never invisible.
1319
+ *
1320
+ * Once headers are on the wire the status can't change, so the socket is
1321
+ * destroyed: a clean `end()` reads as EOF and the client treats a truncated
1322
+ * stream as a complete response. The chat-style SSE routes whose clients
1323
+ * surface an error frame pass `streamEvent`; that frame is written first,
1324
+ * but the tear-down still follows it.
1325
+ *
1326
+ * A body that blew its size cap is the CALLER's fault, not a server fault:
1327
+ * it logs at warn, but if the reader destroyed the socket it journals an
1328
+ * interruption with status 0 because no 4xx reached the client. A client
1329
+ * that hung up before its body arrived
1330
+ * ({@link clientAbortedMidBody}) is not a fault at all: it logs at warn and
1331
+ * journals a torn socket (`interrupted`, status 0), never a 500 the client
1332
+ * could not have received.
1333
+ */
1334
+ function routeError(req, res, err, pathname, opts) {
1335
+ const route = `${req.method ?? "?"} ${pathname}`;
1336
+ const msg = err instanceof Error ? err.message : "Internal error";
1337
+ const clientFault = err instanceof RequestBodyTooLargeError;
1338
+ const clientAbort = clientAbortedMidBody(req, res, err);
1339
+ const status = clientFault ? 400 : 500;
1340
+ if (clientAbort) logger.warn(`${route}: client aborted mid-body (${msg})`);
1341
+ else if (clientFault) logger.warn(`${route}: ${msg}`);
1342
+ else logger.error(`${route}: ${msg}`);
1343
+ if (err instanceof Error && err.stack) logger.debug(err.stack);
1344
+ const existing = lastJournalEntryFor(req);
1345
+ const bodyLimitDisconnect = clientFault && (req.destroyed || res.destroyed);
1346
+ if ((clientAbort || bodyLimitDisconnect) && !res.headersSent) {
1347
+ const interruptReason = bodyLimitDisconnect ? "request body exceeded size limit" : "client aborted";
1348
+ if (existing) {
1349
+ if (!existing.response.interrupted) {
1350
+ existing.response.status = 0;
1351
+ existing.response.interrupted = true;
1352
+ existing.response.interruptReason = interruptReason;
1353
+ }
1354
+ return;
1355
+ }
1356
+ try {
1357
+ journal.add({
1358
+ method: req.method ?? "?",
1359
+ path: req.url ?? "?",
1360
+ headers: flattenHeaders(req.headers),
1361
+ body: null,
1362
+ ...opts?.service ? { service: opts.service } : {},
1363
+ response: {
1364
+ status: 0,
1365
+ fixture: null,
1366
+ source: "internal",
1367
+ interrupted: true,
1368
+ interruptReason
1369
+ }
1370
+ });
1371
+ } catch (jErr) {
1372
+ logger.warn(`${route}: journal write failed after body-read interruption: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
1373
+ }
1374
+ return;
1375
+ }
1376
+ if (!res.headersSent) {
1377
+ if (existing) existing.response.status = status;
1378
+ else try {
1379
+ journal.add({
1380
+ method: req.method ?? "?",
1381
+ path: req.url ?? "?",
1382
+ headers: flattenHeaders(req.headers),
1383
+ body: null,
1384
+ ...opts?.service ? { service: opts.service } : {},
1385
+ response: {
1386
+ status,
1387
+ fixture: null,
1388
+ source: "internal"
1389
+ }
1390
+ });
1391
+ } catch (jErr) {
1392
+ logger.warn(`${route}: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
1393
+ }
1394
+ setCorsHeaders(res);
1395
+ writeErrorResponse(res, status, opts?.envelope ? opts.envelope(msg, status) : JSON.stringify({ error: {
1396
+ message: msg,
1397
+ type: errorTypeForStatus(status)
1398
+ } }));
1399
+ } else {
1400
+ const midStreamReason = clientAbort ? "client aborted" : "handler crashed mid-stream";
1401
+ if (existing) {
1402
+ if (res.writableEnded) {
1403
+ if (existing.response.error === void 0) existing.response.error = msg;
1404
+ } else if (!existing.response.interrupted) {
1405
+ existing.response.interrupted = true;
1406
+ existing.response.interruptReason = midStreamReason;
1407
+ }
1408
+ } else try {
1409
+ journal.add({
1410
+ method: req.method ?? "?",
1411
+ path: req.url ?? "?",
1412
+ headers: flattenHeaders(req.headers),
1413
+ body: null,
1414
+ ...opts?.service ? { service: opts.service } : {},
1415
+ response: {
1416
+ status: res.statusCode,
1417
+ fixture: null,
1418
+ source: "internal",
1419
+ ...res.writableEnded ? { error: msg } : {
1420
+ interrupted: true,
1421
+ interruptReason: midStreamReason
1422
+ }
1423
+ }
1424
+ });
1425
+ } catch (jErr) {
1426
+ logger.warn(`${route}: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
1427
+ }
1428
+ if (res.writableEnded) return;
1429
+ if (opts?.streamEvent) try {
1430
+ res.write(opts.streamEvent(msg, status), () => {
1431
+ if (!res.destroyed) res.destroy();
1432
+ });
1433
+ } catch (writeErr) {
1434
+ logger.debug("Failed to write error recovery response:", writeErr);
1435
+ res.destroy();
1436
+ }
1437
+ else res.destroy();
1438
+ }
1439
+ }
1181
1440
  if (mounts) for (const { handler } of mounts) {
1182
1441
  if (handler.setJournal) handler.setJournal(journal);
1183
1442
  if (registry && handler.setRegistry) handler.setRegistry(registry);
1184
1443
  }
1185
1444
  if (registry) registry.setGauge("aimock_fixtures_loaded", {}, fixtures.length);
1186
1445
  const server = http$1.createServer((req, res) => {
1187
- handleHttpRequest(req, res).catch((err) => {
1188
- const msg = err instanceof Error ? err.message : "Internal error";
1189
- const stack = err instanceof Error ? err.stack ?? msg : msg;
1190
- const method = req.method ?? "?";
1191
- const url = req.url ?? "?";
1192
- defaults.logger.warn(`Unhandled request error on ${method} ${url}: ${msg}\n${stack}`);
1193
- if (!res.headersSent) {
1194
- res.writeHead(500, { "Content-Type": "application/json" });
1195
- res.end(JSON.stringify({ error: {
1196
- message: msg,
1197
- type: "server_error"
1198
- } }));
1199
- } else if (!res.writableEnded) res.end();
1446
+ requestScope.run(req, () => {
1447
+ handleHttpRequest(req, res).catch((err) => {
1448
+ routeError(req, res, err, req.url ?? "?");
1449
+ });
1200
1450
  });
1201
1451
  });
1202
1452
  async function handleHttpRequest(req, res) {
1203
1453
  const startTime = registry ? process.hrtime.bigint() : 0n;
1454
+ const { id: requestId, generated: requestIdMinted } = resolveRequestId(req.headers);
1455
+ req.headers["x-request-id"] = requestId;
1456
+ markMintedRequestId(req, requestIdMinted);
1457
+ res.setHeader("X-Request-Id", requestId);
1458
+ let pathname = "";
1459
+ if (registry) {
1460
+ let recorded = false;
1461
+ const recordOutcome = (status) => {
1462
+ if (recorded) return;
1463
+ recorded = true;
1464
+ try {
1465
+ const normalizedPath = normalizePathLabel(pathname, mounts ? mounts.map((m) => m.path) : []);
1466
+ const method = req.method ?? "UNKNOWN";
1467
+ registry.incrementCounter("aimock_requests_total", {
1468
+ method,
1469
+ path: normalizedPath,
1470
+ status
1471
+ });
1472
+ const elapsed = Number(process.hrtime.bigint() - startTime) / 1e9;
1473
+ registry.observeHistogram("aimock_request_duration_seconds", {
1474
+ method,
1475
+ path: normalizedPath
1476
+ }, elapsed);
1477
+ } catch (err) {
1478
+ defaults.logger.warn("metrics instrumentation error", err);
1479
+ }
1480
+ };
1481
+ res.on("finish", () => recordOutcome(String(res.statusCode)));
1482
+ res.on("close", () => {
1483
+ if (!res.writableFinished) recordOutcome(DESTROYED_STATUS_LABEL);
1484
+ });
1485
+ }
1204
1486
  const parsedUrl = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
1205
- let pathname = parsedUrl.pathname;
1487
+ pathname = parsedUrl.pathname;
1206
1488
  const originalPathname = pathname;
1207
1489
  const isOpenRouter = isOpenRouterPath(originalPathname);
1208
1490
  const isBytePlusArk = defaults.record?.providers.byteplus !== void 0 && originalPathname.startsWith("/api/v3/");
1209
- if (registry) res.on("finish", () => {
1210
- try {
1211
- const normalizedPath = normalizePathLabel(pathname);
1212
- const method = req.method ?? "UNKNOWN";
1213
- const status = String(res.statusCode);
1214
- registry.incrementCounter("aimock_requests_total", {
1215
- method,
1216
- path: normalizedPath,
1217
- status
1218
- });
1219
- const elapsed = Number(process.hrtime.bigint() - startTime) / 1e9;
1220
- registry.observeHistogram("aimock_request_duration_seconds", {
1221
- method,
1222
- path: normalizedPath
1223
- }, elapsed);
1224
- } catch (err) {
1225
- defaults.logger.warn("metrics instrumentation error", err);
1226
- }
1227
- });
1228
1491
  if (req.method === "OPTIONS" && (!resolvedAuth.policy.enabled || req.headers.origin !== void 0 && req.headers["access-control-request-method"] !== void 0)) {
1229
1492
  handleOptions(res);
1230
1493
  return;
@@ -1250,12 +1513,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1250
1513
  try {
1251
1514
  await handleOllama(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
1252
1515
  } catch (err) {
1253
- const msg = err instanceof Error ? err.message : "Internal error";
1254
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
1255
- message: msg,
1256
- type: "server_error"
1257
- } }));
1258
- else if (!res.writableEnded) res.destroy();
1516
+ routeError(req, res, err, pathname);
1259
1517
  }
1260
1518
  return;
1261
1519
  }
@@ -1263,12 +1521,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1263
1521
  try {
1264
1522
  await handleOllamaGenerate(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
1265
1523
  } catch (err) {
1266
- const msg = err instanceof Error ? err.message : "Internal error";
1267
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
1268
- message: msg,
1269
- type: "server_error"
1270
- } }));
1271
- else if (!res.writableEnded) res.destroy();
1524
+ routeError(req, res, err, pathname);
1272
1525
  }
1273
1526
  return;
1274
1527
  }
@@ -1276,12 +1529,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1276
1529
  try {
1277
1530
  await handleOllamaEmbeddings(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
1278
1531
  } catch (err) {
1279
- const msg = err instanceof Error ? err.message : "Internal error";
1280
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
1281
- message: msg,
1282
- type: "server_error"
1283
- } }));
1284
- else if (!res.writableEnded) res.destroy();
1532
+ routeError(req, res, err, pathname);
1285
1533
  }
1286
1534
  return;
1287
1535
  }
@@ -1306,28 +1554,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1306
1554
  try {
1307
1555
  await handleOpenRouterVideoContent(req, res, openRouterVideoContentMatch[1], journal, defaults, setCorsHeaders, openRouterVideoJobs);
1308
1556
  } catch (err) {
1309
- const msg = err instanceof Error ? err.message : "Internal error";
1310
- defaults.logger.error(`openrouter-video content: ${msg}`);
1311
- if (!res.headersSent) {
1312
- try {
1313
- journal.add({
1314
- method: req.method ?? "GET",
1315
- path: req.url ?? pathname,
1316
- headers: flattenHeaders(req.headers),
1317
- body: null,
1318
- response: {
1319
- status: 500,
1320
- fixture: null
1321
- }
1322
- });
1323
- } catch (jErr) {
1324
- defaults.logger.warn(`openrouter-video content: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
1325
- }
1326
- writeErrorResponse(res, 500, JSON.stringify({ error: {
1327
- message: msg,
1328
- type: "server_error"
1329
- } }));
1330
- } else if (!res.writableEnded) res.destroy();
1557
+ routeError(req, res, err, pathname);
1331
1558
  }
1332
1559
  return;
1333
1560
  }
@@ -1335,28 +1562,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1335
1562
  try {
1336
1563
  await handleOpenRouterVideoModels(req, res, fixtures, journal, defaults, setCorsHeaders);
1337
1564
  } catch (err) {
1338
- const msg = err instanceof Error ? err.message : "Internal error";
1339
- defaults.logger.error(`openrouter-video models: ${msg}`);
1340
- if (!res.headersSent) {
1341
- try {
1342
- journal.add({
1343
- method: req.method ?? "GET",
1344
- path: req.url ?? pathname,
1345
- headers: flattenHeaders(req.headers),
1346
- body: null,
1347
- response: {
1348
- status: 500,
1349
- fixture: null
1350
- }
1351
- });
1352
- } catch (jErr) {
1353
- defaults.logger.warn(`openrouter-video models: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
1354
- }
1355
- writeErrorResponse(res, 500, JSON.stringify({ error: {
1356
- message: msg,
1357
- type: "server_error"
1358
- } }));
1359
- } else if (!res.writableEnded) res.destroy();
1565
+ routeError(req, res, err, pathname);
1360
1566
  }
1361
1567
  return;
1362
1568
  }
@@ -1365,28 +1571,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1365
1571
  try {
1366
1572
  await handleOpenRouterVideoStatus(req, res, openRouterVideoStatusMatch[1], fixtures, journal, defaults, setCorsHeaders, openRouterVideoJobs);
1367
1573
  } catch (err) {
1368
- const msg = err instanceof Error ? err.message : "Internal error";
1369
- defaults.logger.error(`openrouter-video status: ${msg}`);
1370
- if (!res.headersSent) {
1371
- try {
1372
- journal.add({
1373
- method: req.method ?? "GET",
1374
- path: req.url ?? pathname,
1375
- headers: flattenHeaders(req.headers),
1376
- body: null,
1377
- response: {
1378
- status: 500,
1379
- fixture: null
1380
- }
1381
- });
1382
- } catch (jErr) {
1383
- defaults.logger.warn(`openrouter-video status: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
1384
- }
1385
- writeErrorResponse(res, 500, JSON.stringify({ error: {
1386
- message: msg,
1387
- type: "server_error"
1388
- } }));
1389
- } else if (!res.writableEnded) res.destroy();
1574
+ routeError(req, res, err, pathname);
1390
1575
  }
1391
1576
  return;
1392
1577
  }
@@ -1395,28 +1580,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1395
1580
  try {
1396
1581
  await handleOpenRouterVideoCreate(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders, openRouterVideoJobs);
1397
1582
  } catch (err) {
1398
- const msg = err instanceof Error ? err.message : "Internal error";
1399
- defaults.logger.error(`openrouter-video submit: ${msg}`);
1400
- if (!res.headersSent) {
1401
- try {
1402
- journal.add({
1403
- method: req.method ?? "POST",
1404
- path: req.url ?? pathname,
1405
- headers: flattenHeaders(req.headers),
1406
- body: null,
1407
- response: {
1408
- status: 500,
1409
- fixture: null
1410
- }
1411
- });
1412
- } catch (jErr) {
1413
- defaults.logger.warn(`openrouter-video submit: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
1414
- }
1415
- writeErrorResponse(res, 500, JSON.stringify({ error: {
1416
- message: msg,
1417
- type: "server_error"
1418
- } }));
1419
- } else if (!res.writableEnded) res.destroy();
1583
+ routeError(req, res, err, pathname);
1420
1584
  }
1421
1585
  return;
1422
1586
  }
@@ -1425,28 +1589,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1425
1589
  try {
1426
1590
  await handleBytePlusVideoStatus(req, res, bytePlusVideoStatusMatch[1], fixtures, journal, defaults, setCorsHeaders, bytePlusVideoJobs);
1427
1591
  } catch (err) {
1428
- const msg = err instanceof Error ? err.message : "Internal error";
1429
- defaults.logger.error(`byteplus-video status: ${msg}`);
1430
- if (!res.headersSent) {
1431
- try {
1432
- journal.add({
1433
- method: req.method ?? "GET",
1434
- path: req.url ?? pathname,
1435
- headers: flattenHeaders(req.headers),
1436
- body: null,
1437
- response: {
1438
- status: 500,
1439
- fixture: null
1440
- }
1441
- });
1442
- } catch (jErr) {
1443
- defaults.logger.warn(`byteplus-video status: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
1444
- }
1445
- writeErrorResponse(res, 500, JSON.stringify({ error: {
1446
- message: msg,
1447
- type: "server_error"
1448
- } }));
1449
- } else if (!res.writableEnded) res.destroy();
1592
+ routeError(req, res, err, pathname);
1450
1593
  }
1451
1594
  return;
1452
1595
  }
@@ -1455,28 +1598,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1455
1598
  try {
1456
1599
  await handleBytePlusVideoCreate(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders, bytePlusVideoJobs);
1457
1600
  } catch (err) {
1458
- const msg = err instanceof Error ? err.message : "Internal error";
1459
- defaults.logger.error(`byteplus-video submit: ${msg}`);
1460
- if (!res.headersSent) {
1461
- try {
1462
- journal.add({
1463
- method: req.method ?? "POST",
1464
- path: req.url ?? pathname,
1465
- headers: flattenHeaders(req.headers),
1466
- body: null,
1467
- response: {
1468
- status: 500,
1469
- fixture: null
1470
- }
1471
- });
1472
- } catch (jErr) {
1473
- defaults.logger.warn(`byteplus-video submit: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
1474
- }
1475
- writeErrorResponse(res, 500, JSON.stringify({ error: {
1476
- message: msg,
1477
- type: "server_error"
1478
- } }));
1479
- } else if (!res.writableEnded) res.destroy();
1601
+ routeError(req, res, err, pathname);
1480
1602
  }
1481
1603
  return;
1482
1604
  }
@@ -1554,6 +1676,127 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1554
1676
  }));
1555
1677
  return;
1556
1678
  }
1679
+ const batchesCancelMatch = pathname.match(BATCHES_CANCEL_RE);
1680
+ if (batchesCancelMatch && req.method === "POST") {
1681
+ try {
1682
+ await readBody(req);
1683
+ await handleBatchesCancel(req, res, batchesCancelMatch[1], journal, defaults, setCorsHeaders);
1684
+ } catch (err) {
1685
+ routeError(req, res, err, pathname, { service: "batches" });
1686
+ }
1687
+ return;
1688
+ }
1689
+ const batchesIdMatch = pathname.match(BATCHES_ID_RE);
1690
+ if (batchesIdMatch && req.method === "GET") {
1691
+ try {
1692
+ await handleBatchesRetrieve(req, res, batchesIdMatch[1], journal, defaults, setCorsHeaders);
1693
+ } catch (err) {
1694
+ routeError(req, res, err, pathname, { service: "batches" });
1695
+ }
1696
+ return;
1697
+ }
1698
+ if (pathname === BATCHES_PATH && req.method === "GET") {
1699
+ try {
1700
+ await handleBatchesList(req, res, journal, defaults, setCorsHeaders);
1701
+ } catch (err) {
1702
+ routeError(req, res, err, pathname, { service: "batches" });
1703
+ }
1704
+ return;
1705
+ }
1706
+ if (pathname === BATCHES_PATH && req.method === "POST") {
1707
+ try {
1708
+ await handleBatchesCreate(req, res, await readBody(req), journal, defaults, setCorsHeaders);
1709
+ } catch (err) {
1710
+ routeError(req, res, err, pathname, { service: "batches" });
1711
+ }
1712
+ return;
1713
+ }
1714
+ const filesContentMatch = pathname.match(FILES_CONTENT_RE);
1715
+ if (filesContentMatch && req.method === "GET") {
1716
+ try {
1717
+ await handleFilesContent(req, res, filesContentMatch[1], journal, defaults, setCorsHeaders);
1718
+ } catch (err) {
1719
+ routeError(req, res, err, pathname, { service: "files" });
1720
+ }
1721
+ return;
1722
+ }
1723
+ const filesIdMatch = pathname.match(FILES_ID_RE);
1724
+ if (filesIdMatch && req.method === "GET") {
1725
+ try {
1726
+ await handleFilesRetrieve(req, res, filesIdMatch[1], journal, defaults, setCorsHeaders);
1727
+ } catch (err) {
1728
+ routeError(req, res, err, pathname, { service: "files" });
1729
+ }
1730
+ return;
1731
+ }
1732
+ if (filesIdMatch && req.method === "DELETE") {
1733
+ try {
1734
+ await handleFilesDelete(req, res, filesIdMatch[1], journal, defaults, setCorsHeaders);
1735
+ } catch (err) {
1736
+ routeError(req, res, err, pathname, { service: "files" });
1737
+ }
1738
+ return;
1739
+ }
1740
+ if (pathname === FILES_PATH && req.method === "GET") {
1741
+ try {
1742
+ await handleFilesList(req, res, journal, defaults, setCorsHeaders);
1743
+ } catch (err) {
1744
+ routeError(req, res, err, pathname, { service: "files" });
1745
+ }
1746
+ return;
1747
+ }
1748
+ if (pathname === FILES_PATH && req.method === "POST") {
1749
+ try {
1750
+ await handleFilesCreate(req, res, (await readBodyBufferBounded(req, FILES_BODY_MAX_BYTES, FILES_BODY_DRAIN_MAX_BYTES)).buffer ?? FILES_BODY_OVERSIZED, journal, defaults, setCorsHeaders);
1751
+ } catch (err) {
1752
+ routeError(req, res, err, pathname, { service: "files" });
1753
+ }
1754
+ return;
1755
+ }
1756
+ const ftCancelMatch = pathname.match(FINE_TUNING_CANCEL_RE);
1757
+ if (ftCancelMatch && req.method === "POST") {
1758
+ try {
1759
+ await readBody(req);
1760
+ await handleFineTuningCancel(req, res, ftCancelMatch[1], journal, defaults, setCorsHeaders);
1761
+ } catch (err) {
1762
+ routeError(req, res, err, pathname, { service: "fine-tuning" });
1763
+ }
1764
+ return;
1765
+ }
1766
+ const ftEventsMatch = pathname.match(FINE_TUNING_EVENTS_RE);
1767
+ if (ftEventsMatch && req.method === "GET") {
1768
+ try {
1769
+ await handleFineTuningEvents(req, res, ftEventsMatch[1], journal, defaults, setCorsHeaders);
1770
+ } catch (err) {
1771
+ routeError(req, res, err, pathname, { service: "fine-tuning" });
1772
+ }
1773
+ return;
1774
+ }
1775
+ const ftIdMatch = pathname.match(FINE_TUNING_ID_RE);
1776
+ if (ftIdMatch && req.method === "GET") {
1777
+ try {
1778
+ await handleFineTuningRetrieve(req, res, ftIdMatch[1], journal, defaults, setCorsHeaders);
1779
+ } catch (err) {
1780
+ routeError(req, res, err, pathname, { service: "fine-tuning" });
1781
+ }
1782
+ return;
1783
+ }
1784
+ if (pathname === FINE_TUNING_JOBS_PATH && req.method === "GET") {
1785
+ try {
1786
+ await handleFineTuningList(req, res, journal, defaults, setCorsHeaders);
1787
+ } catch (err) {
1788
+ routeError(req, res, err, pathname, { service: "fine-tuning" });
1789
+ }
1790
+ return;
1791
+ }
1792
+ if (pathname === FINE_TUNING_JOBS_PATH && req.method === "POST") {
1793
+ try {
1794
+ await handleFineTuningCreate(req, res, await readBody(req), journal, defaults, setCorsHeaders);
1795
+ } catch (err) {
1796
+ routeError(req, res, err, pathname, { service: "fine-tuning" });
1797
+ }
1798
+ return;
1799
+ }
1557
1800
  if (pathname === REQUESTS_PATH) {
1558
1801
  setCorsHeaders(res);
1559
1802
  if (req.method === "GET") {
@@ -1588,17 +1831,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1588
1831
  try {
1589
1832
  await handleResponses(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
1590
1833
  } catch (err) {
1591
- const msg = err instanceof Error ? err.message : "Internal error";
1592
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
1593
- message: msg,
1594
- type: "server_error"
1595
- } }));
1596
- else if (!res.writableEnded) try {
1597
- res.write(`event: error\ndata: ${JSON.stringify({ error: { message: msg } })}\n\n`);
1598
- res.end();
1599
- } catch (writeErr) {
1600
- logger.debug("Failed to write error recovery response:", writeErr);
1601
- }
1834
+ routeError(req, res, err, pathname, { streamEvent: (m) => `event: error\ndata: ${JSON.stringify({ error: { message: m } })}\n\n` });
1602
1835
  }
1603
1836
  return;
1604
1837
  }
@@ -1606,17 +1839,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1606
1839
  try {
1607
1840
  await handleMessages(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
1608
1841
  } catch (err) {
1609
- const msg = err instanceof Error ? err.message : "Internal error";
1610
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
1611
- message: msg,
1612
- type: "server_error"
1613
- } }));
1614
- else if (!res.writableEnded) try {
1615
- res.write(`event: error\ndata: ${JSON.stringify({ error: { message: msg } })}\n\n`);
1616
- res.end();
1617
- } catch (writeErr) {
1618
- logger.debug("Failed to write error recovery response:", writeErr);
1619
- }
1842
+ routeError(req, res, err, pathname, { streamEvent: (m) => `event: error\ndata: ${JSON.stringify({ error: { message: m } })}\n\n` });
1620
1843
  }
1621
1844
  return;
1622
1845
  }
@@ -1624,17 +1847,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1624
1847
  try {
1625
1848
  await handleCohere(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
1626
1849
  } catch (err) {
1627
- const msg = err instanceof Error ? err.message : "Internal error";
1628
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
1629
- message: msg,
1630
- type: "server_error"
1631
- } }));
1632
- else if (!res.writableEnded) try {
1633
- res.write(`event: error\ndata: ${JSON.stringify({ error: { message: msg } })}\n\n`);
1634
- res.end();
1635
- } catch (writeErr) {
1636
- logger.debug("Failed to write error recovery response:", writeErr);
1637
- }
1850
+ routeError(req, res, err, pathname, { streamEvent: (m) => `event: error\ndata: ${JSON.stringify({ error: { message: m } })}\n\n` });
1638
1851
  }
1639
1852
  return;
1640
1853
  }
@@ -1642,12 +1855,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1642
1855
  try {
1643
1856
  await handleCohereEmbed(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
1644
1857
  } catch (err) {
1645
- const msg = err instanceof Error ? err.message : "Internal error";
1646
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
1647
- message: msg,
1648
- type: "server_error"
1649
- } }));
1650
- else if (!res.writableEnded) res.destroy();
1858
+ routeError(req, res, err, pathname);
1651
1859
  }
1652
1860
  return;
1653
1861
  }
@@ -1667,12 +1875,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1667
1875
  }
1668
1876
  await handleEmbeddings(req, res, raw, fixtures, journal, defaults, setCorsHeaders, embeddingsProvider);
1669
1877
  } catch (err) {
1670
- const msg = err instanceof Error ? err.message : "Internal error";
1671
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
1672
- message: msg,
1673
- type: "server_error"
1674
- } }));
1675
- else if (!res.writableEnded) res.destroy();
1878
+ routeError(req, res, err, pathname);
1676
1879
  }
1677
1880
  return;
1678
1881
  }
@@ -1680,12 +1883,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1680
1883
  try {
1681
1884
  await handleImages(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders, "openai", void 0, isBytePlusArk ? "byteplus" : void 0);
1682
1885
  } catch (err) {
1683
- const msg = err instanceof Error ? err.message : "Internal error";
1684
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
1685
- message: msg,
1686
- type: "server_error"
1687
- } }));
1688
- else if (!res.writableEnded) res.destroy();
1886
+ routeError(req, res, err, pathname);
1689
1887
  }
1690
1888
  return;
1691
1889
  }
@@ -1693,12 +1891,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1693
1891
  try {
1694
1892
  await handleImageEdit(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
1695
1893
  } catch (err) {
1696
- const msg = err instanceof Error ? err.message : "Internal error";
1697
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
1698
- message: msg,
1699
- type: "server_error"
1700
- } }));
1701
- else if (!res.writableEnded) res.destroy();
1894
+ routeError(req, res, err, pathname);
1702
1895
  }
1703
1896
  return;
1704
1897
  }
@@ -1706,12 +1899,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1706
1899
  try {
1707
1900
  await handleImageVariations(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
1708
1901
  } catch (err) {
1709
- const msg = err instanceof Error ? err.message : "Internal error";
1710
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
1711
- message: msg,
1712
- type: "server_error"
1713
- } }));
1714
- else if (!res.writableEnded) res.destroy();
1902
+ routeError(req, res, err, pathname);
1715
1903
  }
1716
1904
  return;
1717
1905
  }
@@ -1719,12 +1907,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1719
1907
  try {
1720
1908
  await handleSpeech(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
1721
1909
  } catch (err) {
1722
- const msg = err instanceof Error ? err.message : "Internal error";
1723
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
1724
- message: msg,
1725
- type: "server_error"
1726
- } }));
1727
- else if (!res.writableEnded) res.destroy();
1910
+ routeError(req, res, err, pathname);
1728
1911
  }
1729
1912
  return;
1730
1913
  }
@@ -1732,12 +1915,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1732
1915
  try {
1733
1916
  await handleTranscription(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
1734
1917
  } catch (err) {
1735
- const msg = err instanceof Error ? err.message : "Internal error";
1736
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
1737
- message: msg,
1738
- type: "server_error"
1739
- } }));
1740
- else if (!res.writableEnded) res.destroy();
1918
+ routeError(req, res, err, pathname);
1741
1919
  }
1742
1920
  return;
1743
1921
  }
@@ -1745,12 +1923,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1745
1923
  try {
1746
1924
  await handleTranscription(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders, "translation");
1747
1925
  } catch (err) {
1748
- const msg = err instanceof Error ? err.message : "Internal error";
1749
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
1750
- message: msg,
1751
- type: "server_error"
1752
- } }));
1753
- else if (!res.writableEnded) res.destroy();
1926
+ routeError(req, res, err, pathname);
1754
1927
  }
1755
1928
  return;
1756
1929
  }
@@ -1759,28 +1932,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1759
1932
  try {
1760
1933
  await handleGrokVideoCreate(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders, grokVideoJobs);
1761
1934
  } catch (err) {
1762
- const msg = err instanceof Error ? err.message : "Internal error";
1763
- defaults.logger.error(`grok-video submit: ${msg}`);
1764
- if (!res.headersSent) {
1765
- try {
1766
- journal.add({
1767
- method: req.method ?? "POST",
1768
- path: req.url ?? pathname,
1769
- headers: flattenHeaders(req.headers),
1770
- body: null,
1771
- response: {
1772
- status: 500,
1773
- fixture: null
1774
- }
1775
- });
1776
- } catch (jErr) {
1777
- defaults.logger.warn(`grok-video submit: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
1778
- }
1779
- writeErrorResponse(res, 500, JSON.stringify({ error: {
1780
- message: msg,
1781
- type: "server_error"
1782
- } }));
1783
- } else if (!res.writableEnded) res.destroy();
1935
+ routeError(req, res, err, pathname);
1784
1936
  }
1785
1937
  return;
1786
1938
  }
@@ -1788,12 +1940,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1788
1940
  try {
1789
1941
  await handleVideoCreate(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders, videoStates);
1790
1942
  } catch (err) {
1791
- const msg = err instanceof Error ? err.message : "Internal error";
1792
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
1793
- message: msg,
1794
- type: "server_error"
1795
- } }));
1796
- else if (!res.writableEnded) res.destroy();
1943
+ routeError(req, res, err, pathname);
1797
1944
  }
1798
1945
  return;
1799
1946
  }
@@ -1802,28 +1949,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1802
1949
  try {
1803
1950
  await handleGrokVideoStatus(req, res, grokVideoStatusMatch[1], fixtures, journal, defaults, setCorsHeaders, grokVideoJobs, videoStates);
1804
1951
  } catch (err) {
1805
- const msg = err instanceof Error ? err.message : "Internal error";
1806
- defaults.logger.error(`grok-video status: ${msg}`);
1807
- if (!res.headersSent) {
1808
- try {
1809
- journal.add({
1810
- method: req.method ?? "GET",
1811
- path: req.url ?? pathname,
1812
- headers: flattenHeaders(req.headers),
1813
- body: null,
1814
- response: {
1815
- status: 500,
1816
- fixture: null
1817
- }
1818
- });
1819
- } catch (jErr) {
1820
- defaults.logger.warn(`grok-video status: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
1821
- }
1822
- writeErrorResponse(res, 500, JSON.stringify({ error: {
1823
- message: msg,
1824
- type: "server_error"
1825
- } }));
1826
- } else if (!res.writableEnded) res.destroy();
1952
+ routeError(req, res, err, pathname);
1827
1953
  }
1828
1954
  return;
1829
1955
  }
@@ -1833,28 +1959,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1833
1959
  try {
1834
1960
  await handleVeoVideoCreate(req, res, await readBody(req), veoPredictMatch[1], fixtures, journal, defaults, setCorsHeaders, veoVideoJobs);
1835
1961
  } catch (err) {
1836
- const msg = err instanceof Error ? err.message : "Internal error";
1837
- defaults.logger.error(`veo-video submit: ${msg}`);
1838
- if (!res.headersSent) {
1839
- try {
1840
- journal.add({
1841
- method: req.method ?? "POST",
1842
- path: req.url ?? pathname,
1843
- headers: flattenHeaders(req.headers),
1844
- body: null,
1845
- response: {
1846
- status: 500,
1847
- fixture: null
1848
- }
1849
- });
1850
- } catch (jErr) {
1851
- defaults.logger.warn(`veo-video submit: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
1852
- }
1853
- writeErrorResponse(res, 500, JSON.stringify({ error: {
1854
- message: msg,
1855
- type: "server_error"
1856
- } }));
1857
- } else if (!res.writableEnded) res.destroy();
1962
+ routeError(req, res, err, pathname);
1858
1963
  }
1859
1964
  return;
1860
1965
  }
@@ -1863,28 +1968,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1863
1968
  try {
1864
1969
  await handleVeoVideoStatus(req, res, veoOperationMatch[1], fixtures, journal, defaults, setCorsHeaders, veoVideoJobs);
1865
1970
  } catch (err) {
1866
- const msg = err instanceof Error ? err.message : "Internal error";
1867
- defaults.logger.error(`veo-video status: ${msg}`);
1868
- if (!res.headersSent) {
1869
- try {
1870
- journal.add({
1871
- method: req.method ?? "GET",
1872
- path: req.url ?? pathname,
1873
- headers: flattenHeaders(req.headers),
1874
- body: null,
1875
- response: {
1876
- status: 500,
1877
- fixture: null
1878
- }
1879
- });
1880
- } catch (jErr) {
1881
- defaults.logger.warn(`veo-video status: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
1882
- }
1883
- writeErrorResponse(res, 500, JSON.stringify({ error: {
1884
- message: msg,
1885
- type: "server_error"
1886
- } }));
1887
- } else if (!res.writableEnded) res.destroy();
1971
+ routeError(req, res, err, pathname);
1888
1972
  }
1889
1973
  return;
1890
1974
  }
@@ -1894,12 +1978,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1894
1978
  try {
1895
1979
  await handleImages(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders, "gemini", predictModel);
1896
1980
  } catch (err) {
1897
- const msg = err instanceof Error ? err.message : "Internal error";
1898
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
1899
- message: msg,
1900
- type: "server_error"
1901
- } }));
1902
- else if (!res.writableEnded) res.destroy();
1981
+ routeError(req, res, err, pathname);
1903
1982
  }
1904
1983
  return;
1905
1984
  }
@@ -1907,17 +1986,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1907
1986
  try {
1908
1987
  await handleGeminiInteractions(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
1909
1988
  } catch (err) {
1910
- const msg = err instanceof Error ? err.message : "Internal error";
1911
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
1912
- message: msg,
1913
- type: "server_error"
1914
- } }));
1915
- else if (!res.writableEnded) try {
1916
- res.write(`data: ${JSON.stringify({ error: { message: msg } })}\n\n`);
1917
- res.end();
1918
- } catch (writeErr) {
1919
- logger.debug("Failed to write error recovery response:", writeErr);
1920
- }
1989
+ routeError(req, res, err, pathname, { streamEvent: (m) => `data: ${JSON.stringify({ error: { message: m } })}\n\n` });
1921
1990
  }
1922
1991
  return;
1923
1992
  }
@@ -1927,12 +1996,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1927
1996
  try {
1928
1997
  await handleGeminiEmbedContent(req, res, await readBody(req), embedModel, fixtures, journal, defaults, setCorsHeaders);
1929
1998
  } catch (err) {
1930
- const msg = err instanceof Error ? err.message : "Internal error";
1931
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
1932
- message: msg,
1933
- type: "server_error"
1934
- } }));
1935
- else if (!res.writableEnded) res.destroy();
1999
+ routeError(req, res, err, pathname);
1936
2000
  }
1937
2001
  return;
1938
2002
  }
@@ -1943,17 +2007,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1943
2007
  try {
1944
2008
  await handleGemini(req, res, await readBody(req), geminiModel, streaming, fixtures, journal, defaults, setCorsHeaders);
1945
2009
  } catch (err) {
1946
- const msg = err instanceof Error ? err.message : "Internal error";
1947
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
1948
- message: msg,
1949
- type: "server_error"
1950
- } }));
1951
- else if (!res.writableEnded) try {
1952
- res.write(`data: ${JSON.stringify({ error: { message: msg } })}\n\n`);
1953
- res.end();
1954
- } catch (writeErr) {
1955
- logger.debug("Failed to write error recovery response:", writeErr);
1956
- }
2010
+ routeError(req, res, err, pathname, { streamEvent: (m) => `data: ${JSON.stringify({ error: { message: m } })}\n\n` });
1957
2011
  }
1958
2012
  return;
1959
2013
  }
@@ -1964,17 +2018,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1964
2018
  try {
1965
2019
  await handleGemini(req, res, await readBody(req), vertexModel, streaming, fixtures, journal, defaults, setCorsHeaders, "vertexai");
1966
2020
  } catch (err) {
1967
- const msg = err instanceof Error ? err.message : "Internal error";
1968
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
1969
- message: msg,
1970
- type: "server_error"
1971
- } }));
1972
- else if (!res.writableEnded) try {
1973
- res.write(`data: ${JSON.stringify({ error: { message: msg } })}\n\n`);
1974
- res.end();
1975
- } catch (writeErr) {
1976
- logger.debug("Failed to write error recovery response:", writeErr);
1977
- }
2021
+ routeError(req, res, err, pathname, { streamEvent: (m) => `data: ${JSON.stringify({ error: { message: m } })}\n\n` });
1978
2022
  }
1979
2023
  return;
1980
2024
  }
@@ -1984,12 +2028,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1984
2028
  try {
1985
2029
  await handleBedrock(req, res, await readBody(req), bedrockModelId, fixtures, journal, defaults, setCorsHeaders);
1986
2030
  } catch (err) {
1987
- const msg = err instanceof Error ? err.message : "Internal error";
1988
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
1989
- message: msg,
1990
- type: "server_error"
1991
- } }));
1992
- else if (!res.writableEnded) res.destroy();
2031
+ routeError(req, res, err, pathname);
1993
2032
  }
1994
2033
  return;
1995
2034
  }
@@ -1999,12 +2038,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1999
2038
  try {
2000
2039
  await handleBedrockStream(req, res, await readBody(req), bedrockModelId, fixtures, journal, defaults, setCorsHeaders);
2001
2040
  } catch (err) {
2002
- const msg = err instanceof Error ? err.message : "Internal error";
2003
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
2004
- message: msg,
2005
- type: "server_error"
2006
- } }));
2007
- else if (!res.writableEnded) res.destroy();
2041
+ routeError(req, res, err, pathname);
2008
2042
  }
2009
2043
  return;
2010
2044
  }
@@ -2014,12 +2048,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2014
2048
  try {
2015
2049
  await handleConverse(req, res, await readBody(req), converseModelId, fixtures, journal, defaults, setCorsHeaders);
2016
2050
  } catch (err) {
2017
- const msg = err instanceof Error ? err.message : "Internal error";
2018
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
2019
- message: msg,
2020
- type: "server_error"
2021
- } }));
2022
- else if (!res.writableEnded) res.destroy();
2051
+ routeError(req, res, err, pathname);
2023
2052
  }
2024
2053
  return;
2025
2054
  }
@@ -2029,12 +2058,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2029
2058
  try {
2030
2059
  await handleConverseStream(req, res, await readBody(req), converseStreamModelId, fixtures, journal, defaults, setCorsHeaders);
2031
2060
  } catch (err) {
2032
- const msg = err instanceof Error ? err.message : "Internal error";
2033
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
2034
- message: msg,
2035
- type: "server_error"
2036
- } }));
2037
- else if (!res.writableEnded) res.destroy();
2061
+ routeError(req, res, err, pathname);
2038
2062
  }
2039
2063
  return;
2040
2064
  }
@@ -2042,12 +2066,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2042
2066
  try {
2043
2067
  await handleSearch(req, res, await readBody(req), serviceFixtures?.search ?? [], journal, defaults, setCorsHeaders);
2044
2068
  } catch (err) {
2045
- const msg = err instanceof Error ? err.message : "Internal error";
2046
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
2047
- message: msg,
2048
- type: "server_error"
2049
- } }));
2050
- else if (!res.writableEnded) res.destroy();
2069
+ routeError(req, res, err, pathname, { service: "search" });
2051
2070
  }
2052
2071
  return;
2053
2072
  }
@@ -2055,12 +2074,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2055
2074
  try {
2056
2075
  await handleRerank(req, res, await readBody(req), serviceFixtures?.rerank ?? [], journal, defaults, setCorsHeaders);
2057
2076
  } catch (err) {
2058
- const msg = err instanceof Error ? err.message : "Internal error";
2059
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
2060
- message: msg,
2061
- type: "server_error"
2062
- } }));
2063
- else if (!res.writableEnded) res.destroy();
2077
+ routeError(req, res, err, pathname, { service: "rerank" });
2064
2078
  }
2065
2079
  return;
2066
2080
  }
@@ -2068,12 +2082,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2068
2082
  try {
2069
2083
  await handleModeration(req, res, await readBody(req), serviceFixtures?.moderation ?? [], journal, defaults, setCorsHeaders);
2070
2084
  } catch (err) {
2071
- const msg = err instanceof Error ? err.message : "Internal error";
2072
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
2073
- message: msg,
2074
- type: "server_error"
2075
- } }));
2076
- else if (!res.writableEnded) res.destroy();
2085
+ routeError(req, res, err, pathname, { service: "moderation" });
2077
2086
  }
2078
2087
  return;
2079
2088
  }
@@ -2082,12 +2091,57 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2082
2091
  try {
2083
2092
  await handleElevenLabsAudio(req, res, await readBody(req), fixtures, defaults, journal, "sound-generation");
2084
2093
  } catch (err) {
2085
- const msg = err instanceof Error ? err.message : "Internal error";
2086
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
2087
- message: msg,
2088
- type: "server_error"
2094
+ routeError(req, res, err, pathname);
2095
+ }
2096
+ return;
2097
+ }
2098
+ if (pathname === ELEVENLABS_VOICE_DESIGN_PATH && req.method === "POST") {
2099
+ setCorsHeaders(res);
2100
+ try {
2101
+ await handleElevenLabsVoiceDesign(req, res, await readBody(req), fixtures, defaults, journal);
2102
+ } catch (err) {
2103
+ routeError(req, res, err, pathname, { service: "elevenlabs-voice" });
2104
+ }
2105
+ return;
2106
+ }
2107
+ if (pathname === ELEVENLABS_VOICE_CREATE_PATH && req.method === "POST") {
2108
+ setCorsHeaders(res);
2109
+ try {
2110
+ await handleElevenLabsVoiceCreate(req, res, await readBody(req), fixtures, defaults, journal);
2111
+ } catch (err) {
2112
+ routeError(req, res, err, pathname, { service: "elevenlabs-voice" });
2113
+ }
2114
+ return;
2115
+ }
2116
+ const elevenLabsVoiceMatch = pathname.match(ELEVENLABS_VOICE_RE);
2117
+ if (elevenLabsVoiceMatch && (req.method === "GET" || req.method === "DELETE")) {
2118
+ setCorsHeaders(res);
2119
+ let voiceId;
2120
+ try {
2121
+ voiceId = decodeURIComponent(elevenLabsVoiceMatch[1]);
2122
+ } catch {
2123
+ writeErrorResponse(res, 400, JSON.stringify({ error: {
2124
+ message: `Invalid voice id '${elevenLabsVoiceMatch[1]}': malformed percent-encoding`,
2125
+ type: "invalid_request_error"
2089
2126
  } }));
2090
- else if (!res.writableEnded) res.destroy();
2127
+ journal.add({
2128
+ method: req.method ?? "GET",
2129
+ path: req.url ?? pathname,
2130
+ headers: flattenHeaders(req.headers),
2131
+ service: "elevenlabs-voice",
2132
+ body: null,
2133
+ response: {
2134
+ status: 400,
2135
+ fixture: null
2136
+ }
2137
+ });
2138
+ return;
2139
+ }
2140
+ try {
2141
+ if (req.method === "GET") await handleElevenLabsVoiceGet(req, res, voiceId, fixtures, defaults, journal);
2142
+ else await handleElevenLabsVoiceDelete(req, res, voiceId, fixtures, defaults, journal);
2143
+ } catch (err) {
2144
+ routeError(req, res, err, pathname, { service: "elevenlabs-voice" });
2091
2145
  }
2092
2146
  return;
2093
2147
  }
@@ -2098,12 +2152,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2098
2152
  try {
2099
2153
  await handleElevenLabsTTS(req, res, await readBody(req), fixtures, defaults, journal, voiceId);
2100
2154
  } catch (err) {
2101
- const msg = err instanceof Error ? err.message : "Internal error";
2102
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
2103
- message: msg,
2104
- type: "server_error"
2105
- } }));
2106
- else if (!res.writableEnded) res.destroy();
2155
+ routeError(req, res, err, pathname);
2107
2156
  }
2108
2157
  return;
2109
2158
  }
@@ -2114,42 +2163,38 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2114
2163
  try {
2115
2164
  await handleElevenLabsAudio(req, res, await readBody(req), fixtures, defaults, journal, musicSubType);
2116
2165
  } catch (err) {
2117
- const msg = err instanceof Error ? err.message : "Internal error";
2118
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
2119
- message: msg,
2120
- type: "server_error"
2121
- } }));
2122
- else if (!res.writableEnded) res.destroy();
2166
+ routeError(req, res, err, pathname);
2123
2167
  }
2124
2168
  return;
2125
2169
  }
2126
2170
  let falBody;
2127
- if (FAL_PREFIX_RE.test(pathname) && req.headers["x-fal-target-host"]) {
2171
+ if (FAL_ROUTE_RE.test(pathname) && req.headers["x-fal-target-host"]) {
2128
2172
  setCorsHeaders(res);
2129
2173
  try {
2130
2174
  falBody = req.method === "POST" || req.method === "PUT" ? await readBody(req) : "";
2131
2175
  const raw = falBody;
2132
- const chaosAction = evaluateChaos(null, defaults.chaos, req.headers, defaults.logger, req.url);
2133
- if (chaosAction) {
2134
- applyChaosAction(chaosAction, res, null, journal, {
2135
- method: req.method ?? "GET",
2136
- path: pathname,
2137
- headers: flattenHeaders(req.headers),
2138
- body: {
2139
- model: "",
2140
- messages: []
2141
- }
2142
- }, "fixture", defaults.registry);
2143
- return;
2176
+ if (falWillHandle(req, pathname)) {
2177
+ const chaosConfig = resolveChaosConfig(null, defaults.chaos, req.headers, defaults.logger, req.url);
2178
+ await awaitChaosLatency(null, defaults.chaos, req.headers, defaults.logger, req.url, res, chaosConfig);
2179
+ const goneAfterLatency = responseGoneReason(res);
2180
+ if (goneAfterLatency !== null) {
2181
+ defaults.logger.debug(`[chaos] ${req.method ?? "GET"} ${pathname}: ${describeUnwritableReason(goneAfterLatency)} after the latency delay — not served, not journalled`);
2182
+ return;
2183
+ }
2184
+ const chaosAction = evaluateChaos(null, defaults.chaos, req.headers, defaults.logger, req.url, chaosConfig);
2185
+ if (chaosAction) {
2186
+ applyChaosAction(chaosAction, res, null, journal, {
2187
+ method: req.method ?? "GET",
2188
+ path: pathname,
2189
+ headers: flattenHeaders(req.headers),
2190
+ body: null
2191
+ }, "internal", defaults.registry, defaults.logger);
2192
+ return;
2193
+ }
2144
2194
  }
2145
2195
  if (await handleFal(req, res, raw, pathname, fixtures, defaults, journal) === "handled") return;
2146
2196
  } catch (err) {
2147
- const msg = err instanceof Error ? err.message : "Internal error";
2148
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
2149
- message: msg,
2150
- type: "server_error"
2151
- } }));
2152
- else if (!res.writableEnded) res.destroy();
2197
+ routeError(req, res, err, pathname);
2153
2198
  return;
2154
2199
  }
2155
2200
  }
@@ -2158,12 +2203,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2158
2203
  try {
2159
2204
  await handleFalQueue(req, res, falBody ?? await readBody(req), pathname, fixtures, defaults, journal);
2160
2205
  } catch (err) {
2161
- const msg = err instanceof Error ? err.message : "Internal error";
2162
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
2163
- message: msg,
2164
- type: "server_error"
2165
- } }));
2166
- else if (!res.writableEnded) res.destroy();
2206
+ routeError(req, res, err, pathname);
2167
2207
  }
2168
2208
  return;
2169
2209
  }
@@ -2171,27 +2211,26 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2171
2211
  setCorsHeaders(res);
2172
2212
  try {
2173
2213
  const raw = req.method === "POST" || req.method === "PUT" ? falBody ?? await readBody(req) : "{}";
2174
- const chaosAction = evaluateChaos(null, defaults.chaos, req.headers, defaults.logger, req.url);
2214
+ const chaosConfig = resolveChaosConfig(null, defaults.chaos, req.headers, defaults.logger, req.url);
2215
+ await awaitChaosLatency(null, defaults.chaos, req.headers, defaults.logger, req.url, res, chaosConfig);
2216
+ const goneAfterLatency = responseGoneReason(res);
2217
+ if (goneAfterLatency !== null) {
2218
+ defaults.logger.debug(`[chaos] ${req.method ?? "GET"} ${pathname}: ${describeUnwritableReason(goneAfterLatency)} after the latency delay — not served, not journalled`);
2219
+ return;
2220
+ }
2221
+ const chaosAction = evaluateChaos(null, defaults.chaos, req.headers, defaults.logger, req.url, chaosConfig);
2175
2222
  if (chaosAction) {
2176
2223
  applyChaosAction(chaosAction, res, null, journal, {
2177
2224
  method: req.method ?? "GET",
2178
2225
  path: pathname,
2179
2226
  headers: flattenHeaders(req.headers),
2180
- body: {
2181
- model: "",
2182
- messages: []
2183
- }
2184
- }, "fixture", defaults.registry);
2227
+ body: null
2228
+ }, "internal", defaults.registry, defaults.logger);
2185
2229
  return;
2186
2230
  }
2187
2231
  await handleFalQueue(req, res, raw, pathname, fixtures, defaults, journal);
2188
2232
  } catch (err) {
2189
- const msg = err instanceof Error ? err.message : "Internal error";
2190
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
2191
- message: msg,
2192
- type: "server_error"
2193
- } }));
2194
- else if (!res.writableEnded) res.destroy();
2233
+ routeError(req, res, err, pathname);
2195
2234
  }
2196
2235
  return;
2197
2236
  }
@@ -2200,12 +2239,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2200
2239
  try {
2201
2240
  await handleFalQueue(req, res, falBody ?? await readBody(req), pathname, fixtures, defaults, journal);
2202
2241
  } catch (err) {
2203
- const msg = err instanceof Error ? err.message : "Internal error";
2204
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
2205
- message: msg,
2206
- type: "server_error"
2207
- } }));
2208
- else if (!res.writableEnded) res.destroy();
2242
+ routeError(req, res, err, pathname);
2209
2243
  }
2210
2244
  return;
2211
2245
  }
@@ -2221,23 +2255,25 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2221
2255
  try {
2222
2256
  await handleCompletions(req, res, fixtures, journal, defaults, azureDeploymentId, completionsProvider, isOpenRouter);
2223
2257
  } catch (err) {
2224
- const msg = err instanceof Error ? err.message : "Internal error";
2225
- if (!res.headersSent) writeErrorResponse(res, 500, isOpenRouter ? serializeOpenRouterError(500, msg) : JSON.stringify({ error: {
2226
- message: msg,
2227
- type: "server_error"
2228
- } }));
2229
- else if (!res.writableEnded) try {
2230
- res.write(`data: ${isOpenRouter ? serializeOpenRouterError(500, msg) : JSON.stringify({ error: {
2231
- message: msg,
2232
- type: "server_error"
2233
- } })}\n\n`);
2234
- res.end();
2235
- } catch (writeErr) {
2236
- logger.debug("Failed to write error recovery response:", writeErr);
2237
- }
2258
+ routeError(req, res, err, pathname, {
2259
+ envelope: (m, status) => isOpenRouter ? serializeOpenRouterError(status, m) : JSON.stringify({ error: {
2260
+ message: m,
2261
+ type: errorTypeForStatus(status)
2262
+ } }),
2263
+ streamEvent: (m, status) => `data: ${isOpenRouter ? serializeOpenRouterError(status, m) : JSON.stringify({ error: {
2264
+ message: m,
2265
+ type: errorTypeForStatus(status)
2266
+ } })}\n\n`
2267
+ });
2238
2268
  }
2239
2269
  }
2240
2270
  const activeConnections = /* @__PURE__ */ new Set();
2271
+ const liveLimits = normalizeLiveOptions(options?.live);
2272
+ const liveSessions = /* @__PURE__ */ new Map();
2273
+ const closeLiveSessions = (testId) => {
2274
+ for (const session of [...liveSessions.values()]) if (testId === void 0 || session.testId === testId) session.dispose();
2275
+ };
2276
+ liveClosers.set(defaults, closeLiveSessions);
2241
2277
  server.on("upgrade", (req, socket, head) => {
2242
2278
  handleUpgradeRequest(req, socket, head).catch((err) => {
2243
2279
  const msg = err instanceof Error ? err.message : "Internal error";
@@ -2300,17 +2336,38 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2300
2336
  }
2301
2337
  }
2302
2338
  if (!pathname.match(AZURE_DEPLOYMENT_RE)) pathname = normalizeCompatPath(pathname, logger);
2303
- if (pathname !== RESPONSES_PATH && pathname !== REALTIME_PATH && pathname !== GEMINI_LIVE_PATH) {
2339
+ if (pathname !== RESPONSES_PATH && pathname !== REALTIME_PATH && pathname !== GEMINI_LIVE_PATH && pathname !== LIVE_PATH) {
2304
2340
  socket.write("HTTP/1.1 404 Not Found\r\n\r\n");
2305
2341
  socket.destroy();
2306
2342
  return;
2307
2343
  }
2344
+ let liveId;
2345
+ if (pathname === LIVE_PATH) {
2346
+ if (req.method !== "GET" || parsedUrl.searchParams.has("model")) {
2347
+ socket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
2348
+ return;
2349
+ }
2350
+ if (liveSessions.size >= liveLimits.maxSessions) {
2351
+ socket.end("HTTP/1.1 429 Too Many Requests\r\nConnection: close\r\n\r\n");
2352
+ return;
2353
+ }
2354
+ liveId = Symbol("live-session");
2355
+ const reservedId = liveId;
2356
+ liveSessions.set(reservedId, {
2357
+ testId: getTestId(req),
2358
+ dispose: () => {
2359
+ socket.destroy();
2360
+ liveSessions.delete(reservedId);
2361
+ }
2362
+ });
2363
+ }
2308
2364
  if (head.length > 0) socket.unshift(head);
2309
2365
  let ws;
2310
2366
  try {
2311
- ws = upgradeToWebSocket(req, socket);
2367
+ ws = upgradeToWebSocket(req, socket, pathname === LIVE_PATH ? liveLimits : void 0);
2312
2368
  } catch (err) {
2313
2369
  const msg = err instanceof Error ? err.message : "WebSocket upgrade failed";
2370
+ if (liveId !== void 0) liveSessions.delete(liveId);
2314
2371
  logger.error(`WebSocket upgrade error: ${msg}`);
2315
2372
  if (!socket.destroyed) socket.destroy();
2316
2373
  return;
@@ -2324,7 +2381,24 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2324
2381
  activeConnections.delete(ws);
2325
2382
  });
2326
2383
  const wsTestId = getTestId(req);
2327
- if (pathname === RESPONSES_PATH) handleWebSocketResponses(ws, fixtures, journal, {
2384
+ if (pathname === LIVE_PATH && liveId !== void 0) {
2385
+ const sessionId = liveId;
2386
+ const dispose = handleLiveSession(ws, fixtures, journal, {
2387
+ defaults,
2388
+ limits: liveLimits,
2389
+ localApiKeys: resolvedAuth.publicConfig?.apiKeys,
2390
+ testId: wsTestId,
2391
+ headers: req.headers,
2392
+ onDispose: () => {
2393
+ liveSessions.delete(sessionId);
2394
+ activeConnections.delete(ws);
2395
+ }
2396
+ });
2397
+ liveSessions.set(sessionId, {
2398
+ testId: wsTestId,
2399
+ dispose
2400
+ });
2401
+ } else if (pathname === RESPONSES_PATH) handleWebSocketResponses(ws, fixtures, journal, {
2328
2402
  ...defaults,
2329
2403
  model: "gpt-4",
2330
2404
  testId: wsTestId,
@@ -2349,6 +2423,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2349
2423
  }
2350
2424
  const originalClose = server.close.bind(server);
2351
2425
  server.close = function(callback) {
2426
+ closeLiveSessions();
2352
2427
  for (const ws of activeConnections) ws.close(1001, "Server shutting down");
2353
2428
  activeConnections.clear();
2354
2429
  videoStates.clear();
@@ -2372,6 +2447,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2372
2447
  for (const { path: mountPath, handler } of mounts) if (handler.setBaseUrl) handler.setBaseUrl(url + mountPath);
2373
2448
  }
2374
2449
  resolve({
2450
+ closeLiveSessions,
2375
2451
  server,
2376
2452
  journal,
2377
2453
  url,