@copilotkit/aimock 1.42.1 → 1.44.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 (442) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +65 -0
  4. package/README.md +108 -16
  5. package/dist/agui-handler.d.ts.map +1 -1
  6. package/dist/agui-mock.cjs +1 -1
  7. package/dist/agui-mock.cjs.map +1 -1
  8. package/dist/agui-mock.d.cts.map +1 -1
  9. package/dist/agui-mock.d.ts.map +1 -1
  10. package/dist/agui-mock.js +1 -1
  11. package/dist/agui-mock.js.map +1 -1
  12. package/dist/agui-recorder.cjs +33 -11
  13. package/dist/agui-recorder.cjs.map +1 -1
  14. package/dist/agui-recorder.d.cts +10 -1
  15. package/dist/agui-recorder.d.cts.map +1 -1
  16. package/dist/agui-recorder.d.ts +10 -1
  17. package/dist/agui-recorder.d.ts.map +1 -1
  18. package/dist/agui-recorder.js +33 -11
  19. package/dist/agui-recorder.js.map +1 -1
  20. package/dist/agui-types.d.cts +1 -0
  21. package/dist/agui-types.d.cts.map +1 -1
  22. package/dist/agui-types.d.ts +1 -0
  23. package/dist/agui-types.d.ts.map +1 -1
  24. package/dist/aimock-cli.cjs +38 -10
  25. package/dist/aimock-cli.cjs.map +1 -1
  26. package/dist/aimock-cli.d.cts +5 -0
  27. package/dist/aimock-cli.d.cts.map +1 -1
  28. package/dist/aimock-cli.d.ts +5 -0
  29. package/dist/aimock-cli.d.ts.map +1 -1
  30. package/dist/aimock-cli.js +38 -10
  31. package/dist/aimock-cli.js.map +1 -1
  32. package/dist/batches.cjs +458 -0
  33. package/dist/batches.cjs.map +1 -0
  34. package/dist/batches.d.cts +57 -0
  35. package/dist/batches.d.cts.map +1 -0
  36. package/dist/batches.d.ts +57 -0
  37. package/dist/batches.d.ts.map +1 -0
  38. package/dist/batches.js +453 -0
  39. package/dist/batches.js.map +1 -0
  40. package/dist/bedrock-converse.cjs +44 -12
  41. package/dist/bedrock-converse.cjs.map +1 -1
  42. package/dist/bedrock-converse.d.cts.map +1 -1
  43. package/dist/bedrock-converse.d.ts.map +1 -1
  44. package/dist/bedrock-converse.js +46 -14
  45. package/dist/bedrock-converse.js.map +1 -1
  46. package/dist/bedrock.cjs +10 -8
  47. package/dist/bedrock.cjs.map +1 -1
  48. package/dist/bedrock.d.cts.map +1 -1
  49. package/dist/bedrock.d.ts.map +1 -1
  50. package/dist/bedrock.js +12 -10
  51. package/dist/bedrock.js.map +1 -1
  52. package/dist/byteplus-video.cjs +3 -3
  53. package/dist/byteplus-video.cjs.map +1 -1
  54. package/dist/byteplus-video.js +4 -4
  55. package/dist/byteplus-video.js.map +1 -1
  56. package/dist/chaos.cjs +625 -72
  57. package/dist/chaos.cjs.map +1 -1
  58. package/dist/chaos.d.cts +164 -7
  59. package/dist/chaos.d.cts.map +1 -1
  60. package/dist/chaos.d.ts +164 -7
  61. package/dist/chaos.d.ts.map +1 -1
  62. package/dist/chaos.js +616 -74
  63. package/dist/chaos.js.map +1 -1
  64. package/dist/cli.cjs +131 -30
  65. package/dist/cli.cjs.map +1 -1
  66. package/dist/cli.js +132 -31
  67. package/dist/cli.js.map +1 -1
  68. package/dist/cohere.cjs +37 -9
  69. package/dist/cohere.cjs.map +1 -1
  70. package/dist/cohere.d.cts.map +1 -1
  71. package/dist/cohere.d.ts.map +1 -1
  72. package/dist/cohere.js +39 -11
  73. package/dist/cohere.js.map +1 -1
  74. package/dist/config-loader.cjs +1 -1
  75. package/dist/config-loader.js +1 -1
  76. package/dist/constants.cjs +8 -0
  77. package/dist/constants.cjs.map +1 -1
  78. package/dist/constants.d.cts +6 -2
  79. package/dist/constants.d.ts +6 -2
  80. package/dist/constants.js +8 -1
  81. package/dist/constants.js.map +1 -1
  82. package/dist/convert.cjs +1 -1
  83. package/dist/convert.cjs.map +1 -1
  84. package/dist/convert.js +1 -1
  85. package/dist/convert.js.map +1 -1
  86. package/dist/echo-text.cjs +65 -0
  87. package/dist/echo-text.cjs.map +1 -0
  88. package/dist/echo-text.d.cts +22 -0
  89. package/dist/echo-text.d.cts.map +1 -0
  90. package/dist/echo-text.d.ts +22 -0
  91. package/dist/echo-text.d.ts.map +1 -0
  92. package/dist/echo-text.js +65 -0
  93. package/dist/echo-text.js.map +1 -0
  94. package/dist/elevenlabs-audio.cjs +3 -3
  95. package/dist/elevenlabs-audio.cjs.map +1 -1
  96. package/dist/elevenlabs-audio.js +4 -4
  97. package/dist/elevenlabs-audio.js.map +1 -1
  98. package/dist/elevenlabs-voice.cjs +1053 -0
  99. package/dist/elevenlabs-voice.cjs.map +1 -0
  100. package/dist/elevenlabs-voice.d.cts +72 -0
  101. package/dist/elevenlabs-voice.d.cts.map +1 -0
  102. package/dist/elevenlabs-voice.d.ts +72 -0
  103. package/dist/elevenlabs-voice.d.ts.map +1 -0
  104. package/dist/elevenlabs-voice.js +1048 -0
  105. package/dist/elevenlabs-voice.js.map +1 -0
  106. package/dist/embeddings.cjs +26 -5
  107. package/dist/embeddings.cjs.map +1 -1
  108. package/dist/embeddings.d.cts +1 -1
  109. package/dist/embeddings.d.cts.map +1 -1
  110. package/dist/embeddings.d.ts +1 -1
  111. package/dist/embeddings.d.ts.map +1 -1
  112. package/dist/embeddings.js +27 -6
  113. package/dist/embeddings.js.map +1 -1
  114. package/dist/fal-audio.cjs +3 -3
  115. package/dist/fal-audio.cjs.map +1 -1
  116. package/dist/fal-audio.js +4 -4
  117. package/dist/fal-audio.js.map +1 -1
  118. package/dist/fal.cjs +28 -4
  119. package/dist/fal.cjs.map +1 -1
  120. package/dist/fal.d.cts +16 -1
  121. package/dist/fal.d.cts.map +1 -1
  122. package/dist/fal.d.ts +16 -1
  123. package/dist/fal.d.ts.map +1 -1
  124. package/dist/fal.js +28 -5
  125. package/dist/fal.js.map +1 -1
  126. package/dist/files.cjs +1069 -0
  127. package/dist/files.cjs.map +1 -0
  128. package/dist/files.d.cts +68 -0
  129. package/dist/files.d.cts.map +1 -0
  130. package/dist/files.d.ts +68 -0
  131. package/dist/files.d.ts.map +1 -0
  132. package/dist/files.js +1056 -0
  133. package/dist/files.js.map +1 -0
  134. package/dist/fine-tuning.cjs +1033 -0
  135. package/dist/fine-tuning.cjs.map +1 -0
  136. package/dist/fine-tuning.d.cts +96 -0
  137. package/dist/fine-tuning.d.cts.map +1 -0
  138. package/dist/fine-tuning.d.ts +96 -0
  139. package/dist/fine-tuning.d.ts.map +1 -0
  140. package/dist/fine-tuning.js +1026 -0
  141. package/dist/fine-tuning.js.map +1 -0
  142. package/dist/fixture-loader-services.cjs +116 -0
  143. package/dist/fixture-loader-services.cjs.map +1 -0
  144. package/dist/fixture-loader-services.d.cts +48 -0
  145. package/dist/fixture-loader-services.d.cts.map +1 -0
  146. package/dist/fixture-loader-services.d.ts +48 -0
  147. package/dist/fixture-loader-services.d.ts.map +1 -0
  148. package/dist/fixture-loader-services.js +114 -0
  149. package/dist/fixture-loader-services.js.map +1 -0
  150. package/dist/fixture-loader.cjs +0 -0
  151. package/dist/fixture-loader.cjs.map +1 -1
  152. package/dist/fixture-loader.d.cts +131 -5
  153. package/dist/fixture-loader.d.cts.map +1 -1
  154. package/dist/fixture-loader.d.ts +131 -5
  155. package/dist/fixture-loader.d.ts.map +1 -1
  156. package/dist/fixture-loader.js +0 -0
  157. package/dist/fixture-loader.js.map +1 -1
  158. package/dist/gemini-embeddings.cjs +44 -4
  159. package/dist/gemini-embeddings.cjs.map +1 -1
  160. package/dist/gemini-embeddings.js +46 -6
  161. package/dist/gemini-embeddings.js.map +1 -1
  162. package/dist/gemini-interactions.cjs +51 -7
  163. package/dist/gemini-interactions.cjs.map +1 -1
  164. package/dist/gemini-interactions.d.cts.map +1 -1
  165. package/dist/gemini-interactions.d.ts.map +1 -1
  166. package/dist/gemini-interactions.js +53 -9
  167. package/dist/gemini-interactions.js.map +1 -1
  168. package/dist/gemini.cjs +19 -8
  169. package/dist/gemini.cjs.map +1 -1
  170. package/dist/gemini.d.cts.map +1 -1
  171. package/dist/gemini.d.ts.map +1 -1
  172. package/dist/gemini.js +21 -10
  173. package/dist/gemini.js.map +1 -1
  174. package/dist/grok-video.cjs +14 -14
  175. package/dist/grok-video.cjs.map +1 -1
  176. package/dist/grok-video.js +15 -15
  177. package/dist/grok-video.js.map +1 -1
  178. package/dist/helpers.cjs +497 -10
  179. package/dist/helpers.cjs.map +1 -1
  180. package/dist/helpers.d.cts +38 -2
  181. package/dist/helpers.d.cts.map +1 -1
  182. package/dist/helpers.d.ts +38 -2
  183. package/dist/helpers.d.ts.map +1 -1
  184. package/dist/helpers.js +487 -12
  185. package/dist/helpers.js.map +1 -1
  186. package/dist/images.cjs +57 -125
  187. package/dist/images.cjs.map +1 -1
  188. package/dist/images.d.cts.map +1 -1
  189. package/dist/images.d.ts.map +1 -1
  190. package/dist/images.js +58 -126
  191. package/dist/images.js.map +1 -1
  192. package/dist/index.cjs +71 -9
  193. package/dist/index.d.cts +17 -7
  194. package/dist/index.d.ts +17 -7
  195. package/dist/index.js +21 -11
  196. package/dist/jest.cjs +24 -12
  197. package/dist/jest.cjs.map +1 -1
  198. package/dist/jest.d.cts +6 -3
  199. package/dist/jest.d.cts.map +1 -1
  200. package/dist/jest.d.ts +6 -3
  201. package/dist/jest.d.ts.map +1 -1
  202. package/dist/jest.js +25 -13
  203. package/dist/jest.js.map +1 -1
  204. package/dist/journal.cjs +61 -4
  205. package/dist/journal.cjs.map +1 -1
  206. package/dist/journal.d.cts +60 -3
  207. package/dist/journal.d.cts.map +1 -1
  208. package/dist/journal.d.ts +60 -3
  209. package/dist/journal.d.ts.map +1 -1
  210. package/dist/journal.js +61 -5
  211. package/dist/journal.js.map +1 -1
  212. package/dist/live-fixture.cjs +1192 -0
  213. package/dist/live-fixture.cjs.map +1 -0
  214. package/dist/live-fixture.js +1187 -0
  215. package/dist/live-fixture.js.map +1 -0
  216. package/dist/live-recorder.cjs +256 -0
  217. package/dist/live-recorder.cjs.map +1 -0
  218. package/dist/live-recorder.js +254 -0
  219. package/dist/live-recorder.js.map +1 -0
  220. package/dist/live-replay.cjs +286 -0
  221. package/dist/live-replay.cjs.map +1 -0
  222. package/dist/live-replay.js +285 -0
  223. package/dist/live-replay.js.map +1 -0
  224. package/dist/live-sanitize.cjs +196 -0
  225. package/dist/live-sanitize.cjs.map +1 -0
  226. package/dist/live-sanitize.js +196 -0
  227. package/dist/live-sanitize.js.map +1 -0
  228. package/dist/live-types.d.cts +71 -0
  229. package/dist/live-types.d.cts.map +1 -0
  230. package/dist/live-types.d.ts +71 -0
  231. package/dist/live-types.d.ts.map +1 -0
  232. package/dist/llmock.cjs +132 -49
  233. package/dist/llmock.cjs.map +1 -1
  234. package/dist/llmock.d.cts +51 -5
  235. package/dist/llmock.d.cts.map +1 -1
  236. package/dist/llmock.d.ts +51 -5
  237. package/dist/llmock.d.ts.map +1 -1
  238. package/dist/llmock.js +133 -50
  239. package/dist/llmock.js.map +1 -1
  240. package/dist/mcp-fakes-mount.cjs +216 -0
  241. package/dist/mcp-fakes-mount.cjs.map +1 -0
  242. package/dist/mcp-fakes-mount.js +210 -0
  243. package/dist/mcp-fakes-mount.js.map +1 -0
  244. package/dist/mcp-fakes.cjs +1619 -0
  245. package/dist/mcp-fakes.cjs.map +1 -0
  246. package/dist/mcp-fakes.d.cts +101 -0
  247. package/dist/mcp-fakes.d.cts.map +1 -0
  248. package/dist/mcp-fakes.d.ts +101 -0
  249. package/dist/mcp-fakes.d.ts.map +1 -0
  250. package/dist/mcp-fakes.js +1614 -0
  251. package/dist/mcp-fakes.js.map +1 -0
  252. package/dist/mcp-handler.cjs +331 -17
  253. package/dist/mcp-handler.cjs.map +1 -1
  254. package/dist/mcp-handler.js +330 -18
  255. package/dist/mcp-handler.js.map +1 -1
  256. package/dist/mcp-mock.cjs +217 -49
  257. package/dist/mcp-mock.cjs.map +1 -1
  258. package/dist/mcp-mock.d.cts +66 -1
  259. package/dist/mcp-mock.d.cts.map +1 -1
  260. package/dist/mcp-mock.d.ts +66 -1
  261. package/dist/mcp-mock.d.ts.map +1 -1
  262. package/dist/mcp-mock.js +218 -50
  263. package/dist/mcp-mock.js.map +1 -1
  264. package/dist/mcp-stub.cjs +5 -1
  265. package/dist/mcp-stub.d.cts +3 -1
  266. package/dist/mcp-stub.d.ts +3 -1
  267. package/dist/mcp-stub.js +3 -1
  268. package/dist/mcp-types.d.cts +6 -0
  269. package/dist/mcp-types.d.ts +6 -0
  270. package/dist/message-text.cjs +192 -0
  271. package/dist/message-text.cjs.map +1 -0
  272. package/dist/message-text.d.cts +31 -0
  273. package/dist/message-text.d.cts.map +1 -0
  274. package/dist/message-text.d.ts +31 -0
  275. package/dist/message-text.d.ts.map +1 -0
  276. package/dist/message-text.js +184 -0
  277. package/dist/message-text.js.map +1 -0
  278. package/dist/messages.cjs +49 -7
  279. package/dist/messages.cjs.map +1 -1
  280. package/dist/messages.d.cts.map +1 -1
  281. package/dist/messages.d.ts.map +1 -1
  282. package/dist/messages.js +51 -9
  283. package/dist/messages.js.map +1 -1
  284. package/dist/metrics.cjs +278 -45
  285. package/dist/metrics.cjs.map +1 -1
  286. package/dist/metrics.d.cts +17 -7
  287. package/dist/metrics.d.cts.map +1 -1
  288. package/dist/metrics.d.ts +17 -7
  289. package/dist/metrics.d.ts.map +1 -1
  290. package/dist/metrics.js +270 -29
  291. package/dist/metrics.js.map +1 -1
  292. package/dist/moderation.cjs +10 -1
  293. package/dist/moderation.cjs.map +1 -1
  294. package/dist/moderation.d.cts.map +1 -1
  295. package/dist/moderation.d.ts.map +1 -1
  296. package/dist/moderation.js +10 -1
  297. package/dist/moderation.js.map +1 -1
  298. package/dist/ollama.cjs +10 -6
  299. package/dist/ollama.cjs.map +1 -1
  300. package/dist/ollama.d.cts.map +1 -1
  301. package/dist/ollama.d.ts.map +1 -1
  302. package/dist/ollama.js +11 -7
  303. package/dist/ollama.js.map +1 -1
  304. package/dist/openapi.cjs +3405 -0
  305. package/dist/openapi.cjs.map +1 -0
  306. package/dist/openapi.d.cts +30 -0
  307. package/dist/openapi.d.cts.map +1 -0
  308. package/dist/openapi.d.ts +30 -0
  309. package/dist/openapi.d.ts.map +1 -0
  310. package/dist/openapi.js +3404 -0
  311. package/dist/openapi.js.map +1 -0
  312. package/dist/openrouter-video.cjs +9 -9
  313. package/dist/openrouter-video.cjs.map +1 -1
  314. package/dist/openrouter-video.d.cts.map +1 -1
  315. package/dist/openrouter-video.d.ts.map +1 -1
  316. package/dist/openrouter-video.js +10 -10
  317. package/dist/openrouter-video.js.map +1 -1
  318. package/dist/recorder.cjs +54 -10
  319. package/dist/recorder.cjs.map +1 -1
  320. package/dist/recorder.d.cts.map +1 -1
  321. package/dist/recorder.d.ts.map +1 -1
  322. package/dist/recorder.js +52 -12
  323. package/dist/recorder.js.map +1 -1
  324. package/dist/responses.cjs +47 -7
  325. package/dist/responses.cjs.map +1 -1
  326. package/dist/responses.d.cts.map +1 -1
  327. package/dist/responses.d.ts.map +1 -1
  328. package/dist/responses.js +48 -9
  329. package/dist/responses.js.map +1 -1
  330. package/dist/route-registry.cjs +951 -0
  331. package/dist/route-registry.cjs.map +1 -0
  332. package/dist/route-registry.d.cts +33 -0
  333. package/dist/route-registry.d.cts.map +1 -0
  334. package/dist/route-registry.d.ts +33 -0
  335. package/dist/route-registry.d.ts.map +1 -0
  336. package/dist/route-registry.js +866 -0
  337. package/dist/route-registry.js.map +1 -0
  338. package/dist/router.cjs +21 -34
  339. package/dist/router.cjs.map +1 -1
  340. package/dist/router.d.cts +4 -1
  341. package/dist/router.d.cts.map +1 -1
  342. package/dist/router.d.ts +4 -1
  343. package/dist/router.d.ts.map +1 -1
  344. package/dist/router.js +21 -35
  345. package/dist/router.js.map +1 -1
  346. package/dist/server.cjs +1252 -777
  347. package/dist/server.cjs.map +1 -1
  348. package/dist/server.d.cts +5 -1
  349. package/dist/server.d.cts.map +1 -1
  350. package/dist/server.d.ts +5 -1
  351. package/dist/server.d.ts.map +1 -1
  352. package/dist/server.js +1185 -710
  353. package/dist/server.js.map +1 -1
  354. package/dist/speech.cjs +6 -4
  355. package/dist/speech.cjs.map +1 -1
  356. package/dist/speech.js +7 -5
  357. package/dist/speech.js.map +1 -1
  358. package/dist/suite.cjs +1 -1
  359. package/dist/suite.js +1 -1
  360. package/dist/transcription.cjs +6 -6
  361. package/dist/transcription.cjs.map +1 -1
  362. package/dist/transcription.d.cts.map +1 -1
  363. package/dist/transcription.d.ts.map +1 -1
  364. package/dist/transcription.js +7 -7
  365. package/dist/transcription.js.map +1 -1
  366. package/dist/types.cjs +18 -0
  367. package/dist/types.cjs.map +1 -0
  368. package/dist/types.d.cts +320 -17
  369. package/dist/types.d.cts.map +1 -1
  370. package/dist/types.d.ts +320 -17
  371. package/dist/types.d.ts.map +1 -1
  372. package/dist/types.js +17 -0
  373. package/dist/types.js.map +1 -0
  374. package/dist/validate-cli.cjs +983 -0
  375. package/dist/validate-cli.cjs.map +1 -0
  376. package/dist/validate-cli.js +982 -0
  377. package/dist/validate-cli.js.map +1 -0
  378. package/dist/vector-stores.cjs +1180 -0
  379. package/dist/vector-stores.cjs.map +1 -0
  380. package/dist/vector-stores.d.cts +106 -0
  381. package/dist/vector-stores.d.cts.map +1 -0
  382. package/dist/vector-stores.d.ts +106 -0
  383. package/dist/vector-stores.d.ts.map +1 -0
  384. package/dist/vector-stores.js +1164 -0
  385. package/dist/vector-stores.js.map +1 -0
  386. package/dist/vector-types.d.cts.map +1 -1
  387. package/dist/vector-types.d.ts.map +1 -1
  388. package/dist/veo-video.cjs +13 -13
  389. package/dist/veo-video.cjs.map +1 -1
  390. package/dist/veo-video.js +14 -14
  391. package/dist/veo-video.js.map +1 -1
  392. package/dist/video.cjs +23 -4
  393. package/dist/video.cjs.map +1 -1
  394. package/dist/video.d.cts +1 -1
  395. package/dist/video.d.cts.map +1 -1
  396. package/dist/video.d.ts +1 -1
  397. package/dist/video.d.ts.map +1 -1
  398. package/dist/video.js +24 -5
  399. package/dist/video.js.map +1 -1
  400. package/dist/vitest.cjs +23 -11
  401. package/dist/vitest.cjs.map +1 -1
  402. package/dist/vitest.d.cts +5 -2
  403. package/dist/vitest.d.cts.map +1 -1
  404. package/dist/vitest.d.ts +5 -2
  405. package/dist/vitest.d.ts.map +1 -1
  406. package/dist/vitest.js +25 -13
  407. package/dist/vitest.js.map +1 -1
  408. package/dist/watcher.cjs +2 -1
  409. package/dist/watcher.cjs.map +1 -1
  410. package/dist/watcher.js +2 -1
  411. package/dist/watcher.js.map +1 -1
  412. package/dist/ws-framing.cjs +150 -35
  413. package/dist/ws-framing.cjs.map +1 -1
  414. package/dist/ws-framing.d.cts +18 -3
  415. package/dist/ws-framing.d.cts.map +1 -1
  416. package/dist/ws-framing.d.ts +18 -3
  417. package/dist/ws-framing.d.ts.map +1 -1
  418. package/dist/ws-framing.js +151 -36
  419. package/dist/ws-framing.js.map +1 -1
  420. package/dist/ws-gemini-live.cjs +2 -2
  421. package/dist/ws-gemini-live.js +2 -2
  422. package/dist/ws-live.cjs +401 -0
  423. package/dist/ws-live.cjs.map +1 -0
  424. package/dist/ws-live.js +400 -0
  425. package/dist/ws-live.js.map +1 -0
  426. package/dist/ws-realtime.cjs +46 -2
  427. package/dist/ws-realtime.cjs.map +1 -1
  428. package/dist/ws-realtime.d.cts.map +1 -1
  429. package/dist/ws-realtime.d.ts.map +1 -1
  430. package/dist/ws-realtime.js +46 -2
  431. package/dist/ws-realtime.js.map +1 -1
  432. package/dist/ws-responses.cjs +16 -4
  433. package/dist/ws-responses.cjs.map +1 -1
  434. package/dist/ws-responses.js +16 -4
  435. package/dist/ws-responses.js.map +1 -1
  436. package/dist/ws-upstream.cjs +118 -0
  437. package/dist/ws-upstream.cjs.map +1 -0
  438. package/dist/ws-upstream.js +115 -0
  439. package/dist/ws-upstream.js.map +1 -0
  440. package/fixtures/openai-live-client.json +390 -0
  441. package/fixtures/openai-live-managed.json +709 -0
  442. package/package.json +4 -1
package/dist/server.js CHANGED
@@ -1,15 +1,17 @@
1
1
  import { DEFAULT_TEST_ID } from "./constants.js";
2
+ import { build, msg, quote } from "./message-text.js";
2
3
  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";
4
+ 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, validateToolsField, wouldProxyMiss } from "./helpers.js";
5
+ import { normalizeLiveOptions } from "./live-fixture.js";
6
+ import { writeErrorResponse, writeSSEStream } from "./sse-writer.js";
7
+ import { CHAOS_FIELDS, CHAOS_FIELD_NAMES, applyChaosAction, awaitChaosLatency, describeUnwritableReason, evaluateChaos, isChaosScope, parseChaosField, resetChaosWarnings, resolveChaosConfig, responseGoneReason } from "./chaos.js";
8
+ import { FixtureLoadError, INJECTED_STATUS_RANGE, claimOneShotError, clearFixtureQueue, entryToFixture, isInjectableStatus, isOneShotError, queueOneShotError, releaseOneShotError, validateFixtures } from "./fixture-loader.js";
9
+ import { matchFixtureDiagnostic, recordMatchOptions } from "./router.js";
10
+ import { proxyAndRecord } from "./recorder.js";
4
11
  import { Logger } from "./logger.js";
5
12
  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
13
  import { createInterruptionSignal } from "./interruption.js";
10
14
  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
15
  import { handleResponses } from "./responses.js";
14
16
  import { handleMessages } from "./messages.js";
15
17
  import { handleGemini } from "./gemini.js";
@@ -23,51 +25,38 @@ import { handleImageEdit, handleImageVariations, handleImages } from "./images.j
23
25
  import { handleSpeech } from "./speech.js";
24
26
  import { VideoStateMap, handleVideoCreate } from "./video.js";
25
27
  import { falJobs, handleFalQueue } from "./fal-audio.js";
26
- import { falQueueStates, handleFal } from "./fal.js";
28
+ import { falQueueStates, falWillHandle, handleFal } from "./fal.js";
27
29
  import { OPENROUTER_VIDEO_DEFAULT_MAX_CONTENT_BYTES, OpenRouterVideoJobMap, handleOpenRouterVideoContent, handleOpenRouterVideoCreate, handleOpenRouterVideoModels, handleOpenRouterVideoStatus } from "./openrouter-video.js";
28
30
  import { VeoVideoJobMap, handleVeoVideoCreate, handleVeoVideoStatus } from "./veo-video.js";
29
31
  import { GrokVideoJobMap, handleGrokVideoCreate, handleGrokVideoStatus } from "./grok-video.js";
30
32
  import { BytePlusVideoJobMap, handleBytePlusVideoCreate, handleBytePlusVideoStatus } from "./byteplus-video.js";
31
33
  import { handleElevenLabsAudio, handleElevenLabsTTS } from "./elevenlabs-audio.js";
34
+ import { clearElevenLabsVoices, handleElevenLabsVoiceCreate, handleElevenLabsVoiceDelete, handleElevenLabsVoiceDesign, handleElevenLabsVoiceGet } from "./elevenlabs-voice.js";
32
35
  import { handleOllama, handleOllamaEmbeddings, handleOllamaGenerate } from "./ollama.js";
33
36
  import { handleCohere, handleCohereEmbed } from "./cohere.js";
34
37
  import { handleSearch } from "./search.js";
35
38
  import { handleRerank } from "./rerank.js";
36
39
  import { handleModeration } from "./moderation.js";
40
+ import { clearFineTuningStore, handleFineTuningCancel, handleFineTuningCreate, handleFineTuningEvents, handleFineTuningList, handleFineTuningRetrieve } from "./fine-tuning.js";
41
+ import { clearVectorStoreStore, handleVectorFileBatchesCancel, handleVectorFileBatchesCreate, handleVectorFileBatchesFiles, handleVectorFileBatchesRetrieve, handleVectorStoreFilesCreate, handleVectorStoreFilesDelete, handleVectorStoreFilesList, handleVectorStoreFilesRetrieve, handleVectorStoresCreate, handleVectorStoresDelete, handleVectorStoresList, handleVectorStoresModify, handleVectorStoresRetrieve, handleVectorStoresSearch } from "./vector-stores.js";
42
+ import { FILES_BODY_DRAIN_MAX_BYTES, FILES_BODY_MAX_BYTES, FILES_BODY_OVERSIZED, clearFileStore, handleFilesContent, handleFilesCreate, handleFilesDelete, handleFilesList, handleFilesRetrieve } from "./files.js";
43
+ import { clearBatchStore, handleBatchesCancel, handleBatchesCreate, handleBatchesList, handleBatchesRetrieve } from "./batches.js";
37
44
  import { upgradeToWebSocket } from "./ws-framing.js";
45
+ import { handleLiveSession } from "./ws-live.js";
38
46
  import { handleWebSocketResponses } from "./ws-responses.js";
39
47
  import { handleWebSocketRealtime } from "./ws-realtime.js";
40
48
  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";
49
+ import { AZURE_DEPLOYMENT_RE, BATCHES_CANCEL_RE, BATCHES_ID_RE, BATCHES_PATH, BEDROCK_CONVERSE_RE, BEDROCK_CONVERSE_STREAM_RE, BEDROCK_INVOKE_RE, BEDROCK_STREAM_RE, BYTEPLUS_VIDEO_STATUS_RE, BYTEPLUS_VIDEO_SUBMIT_RE, COHERE_CHAT_PATH, COHERE_EMBED_PATH, COMPLETIONS_PATH, CONTROL_PREFIX, ELEVENLABS_MUSIC_RE, ELEVENLABS_SOUND_GENERATION_PATH, ELEVENLABS_TTS_RE, ELEVENLABS_VOICE_CREATE_PATH, ELEVENLABS_VOICE_DESIGN_PATH, ELEVENLABS_VOICE_RE, EMBEDDINGS_PATH, FAL_QUEUE_REQUESTS_RE, FAL_QUEUE_SUBMIT_RE, FAL_ROUTE_RE, FAL_RUN_RE, FILES_CONTENT_RE, FILES_ID_RE, FILES_PATH, FINE_TUNING_CANCEL_RE, FINE_TUNING_EVENTS_RE, FINE_TUNING_ID_RE, FINE_TUNING_JOBS_PATH, GEMINI_EMBED_RE, GEMINI_INTERACTIONS_PATH, GEMINI_LIVE_PATH, GEMINI_PATH_RE, GEMINI_PREDICT_RE, GROK_VIDEO_STATUS_RE, GROK_VIDEO_SUBMIT_PATH, HEALTH_PATH, IMAGES_EDIT_PATH, IMAGES_PATH, IMAGES_VARIATIONS_PATH, LIVE_PATH, MESSAGES_PATH, METRICS_PATH, MODELS_PATH, MODERATIONS_PATH, OLLAMA_CHAT_PATH, OLLAMA_EMBEDDINGS_PATH, OLLAMA_EMBED_PATH, OLLAMA_GENERATE_PATH, OLLAMA_TAGS_PATH, OPENROUTER_CREDITS_PATH, OPENROUTER_KEY_PATH, OPENROUTER_MODELS_PATH, OPENROUTER_VIDEOS_PATH, OPENROUTER_VIDEO_CONTENT_RE, OPENROUTER_VIDEO_MODELS_PATH, OPENROUTER_VIDEO_STATUS_RE, READY_PATH, REALTIME_PATH, REQUESTS_PATH, RERANK_PATH, RESPONSES_PATH, SEARCH_PATH, SPEECH_PATH, TRANSCRIPTIONS_PATH, TRANSLATIONS_PATH, VECTOR_STORES_BATCH_CANCEL_RE, VECTOR_STORES_BATCH_FILES_RE, VECTOR_STORES_BATCH_RE, VECTOR_STORES_FILES_RE, VECTOR_STORES_FILE_BATCHES_RE, VECTOR_STORES_FILE_RE, VECTOR_STORES_ID_RE, VECTOR_STORES_PATH, VECTOR_STORES_SEARCH_RE, VEO_OPERATION_RE, VEO_PREDICT_LRO_RE, VERTEX_AI_RE, VIDEOS_PATH, matchRouteDefinition } from "./route-registry.js";
50
+ import { CATALOG_ROUTES, buildOpenApiDocument } from "./openapi.js";
51
+ import { DESTROYED_STATUS_LABEL, createMetricsRegistry, normalizePathLabel } from "./metrics.js";
52
+ import { MCP_FAKES_DEFAULT_MOUNT, McpFakeStore, McpFakesAddError, blockIdOf } from "./mcp-fakes.js";
53
+ import { MCPMock } from "./mcp-mock.js";
54
+ import { ensureFakeMount, findFakeMount, handOffMcpFakes, isFakeMount, mountConflictDetail } from "./mcp-fakes-mount.js";
42
55
  import * as http$1 from "node:http";
56
+ import { AsyncLocalStorage } from "node:async_hooks";
43
57
 
44
58
  //#region src/server.ts
45
- const COMPLETIONS_PATH = "/v1/chat/completions";
46
- const RESPONSES_PATH = "/v1/responses";
47
- const REALTIME_PATH = "/v1/realtime";
48
- const GEMINI_LIVE_PATH = "/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent";
49
- const MESSAGES_PATH = "/v1/messages";
50
- const EMBEDDINGS_PATH = "/v1/embeddings";
51
- const COHERE_CHAT_PATH = "/v2/chat";
52
- const COHERE_EMBED_PATH = "/v2/embed";
53
- const SEARCH_PATH = "/search";
54
- const RERANK_PATH = "/v2/rerank";
55
- const MODERATIONS_PATH = "/v1/moderations";
56
- const IMAGES_PATH = "/v1/images/generations";
57
- const IMAGES_EDIT_PATH = "/v1/images/edits";
58
- const IMAGES_VARIATIONS_PATH = "/v1/images/variations";
59
- const SPEECH_PATH = "/v1/audio/speech";
60
- const TRANSCRIPTIONS_PATH = "/v1/audio/transcriptions";
61
- const TRANSLATIONS_PATH = "/v1/audio/translations";
62
- const VIDEOS_PATH = "/v1/videos";
63
- const GEMINI_PREDICT_RE = /^\/v1beta\/models\/([^:]+):predict$/;
64
- const ELEVENLABS_SOUND_GENERATION_PATH = "/v1/sound-generation";
65
- const ELEVENLABS_TTS_RE = /^\/v1\/text-to-speech\/([^/]+)$/;
66
- const ELEVENLABS_MUSIC_RE = /^\/v1\/music(?:\/(.+))?$/;
67
- const FAL_QUEUE_SUBMIT_RE = /^\/fal\/queue\/submit\/(.+)$/;
68
- const FAL_QUEUE_REQUESTS_RE = /^\/fal\/queue\/requests\/(.+)$/;
69
- const FAL_RUN_RE = /^\/fal\/run\/(.+)$/;
70
- const FAL_PREFIX_RE = /^\/fal(?:\/.*)?$/;
59
+ const liveClosers = /* @__PURE__ */ new WeakMap();
71
60
  const DEFAULT_CHUNK_SIZE = 20;
72
61
  const COMPAT_SUFFIXES = [
73
62
  "/chat/completions",
@@ -96,26 +85,6 @@ function normalizeCompatPath(pathname, logger) {
96
85
  }
97
86
  return pathname;
98
87
  }
99
- const GEMINI_INTERACTIONS_PATH = "/v1beta/interactions";
100
- const GEMINI_PATH_RE = /^\/v1beta\/models\/([^:]+):(generateContent|streamGenerateContent)$/;
101
- const GEMINI_EMBED_RE = /^\/v1beta\/models\/([^:]+):embedContent$/;
102
- const AZURE_DEPLOYMENT_RE = /^\/openai\/deployments\/([^/]+)\/(chat\/completions|embeddings)$/;
103
- const BEDROCK_INVOKE_RE = /^\/model\/([^/]+)\/invoke$/;
104
- const BEDROCK_STREAM_RE = /^\/model\/([^/]+)\/invoke-with-response-stream$/;
105
- const BEDROCK_CONVERSE_RE = /^\/model\/([^/]+)\/converse$/;
106
- const BEDROCK_CONVERSE_STREAM_RE = /^\/model\/([^/]+)\/converse-stream$/;
107
- const VERTEX_AI_RE = /^\/v1\/projects\/[^/]+\/locations\/[^/]+\/publishers\/google\/models\/([^/:]+):(generateContent|streamGenerateContent)$/;
108
- const OLLAMA_CHAT_PATH = "/api/chat";
109
- const OLLAMA_GENERATE_PATH = "/api/generate";
110
- const OLLAMA_EMBEDDINGS_PATH = "/api/embeddings";
111
- const OLLAMA_EMBED_PATH = "/api/embed";
112
- const OLLAMA_TAGS_PATH = "/api/tags";
113
- const OPENROUTER_VIDEOS_PATH = "/api/v1/videos";
114
- const OPENROUTER_VIDEO_MODELS_PATH = "/api/v1/videos/models";
115
- const HEALTH_PATH = "/health";
116
- const READY_PATH = "/ready";
117
- const MODELS_PATH = "/v1/models";
118
- const REQUESTS_PATH = "/v1/_requests";
119
88
  const DEFAULT_MODELS = [
120
89
  "gpt-4",
121
90
  "gpt-4o",
@@ -125,9 +94,9 @@ const DEFAULT_MODELS = [
125
94
  ];
126
95
  const CORS_HEADERS = {
127
96
  "Access-Control-Allow-Origin": "*",
128
- "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
97
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
129
98
  "Access-Control-Allow-Headers": "*",
130
- "Access-Control-Expose-Headers": "X-Total-Count"
99
+ "Access-Control-Expose-Headers": "X-Total-Count, X-Request-Id"
131
100
  };
132
101
  function setCorsHeaders(res) {
133
102
  for (const [key, value] of Object.entries(CORS_HEADERS)) res.setHeader(key, value);
@@ -144,7 +113,6 @@ function handleNotFound(res, message) {
144
113
  type: "not_found"
145
114
  } }));
146
115
  }
147
- const CONTROL_PREFIX = "/__aimock";
148
116
  /** The complete `GET /__aimock/fixtures` query-param vocabulary; anything else 400s. */
149
117
  const FIXTURES_PARAMS = new Set(["include"]);
150
118
  /** The complete `GET /__aimock/journal` query-param vocabulary; anything else 400s. */
@@ -155,14 +123,18 @@ const JOURNAL_PARAMS = new Set([
155
123
  "method",
156
124
  "status",
157
125
  "service",
158
- "testId"
126
+ "testId",
127
+ "requestId"
159
128
  ]);
160
129
  /**
161
130
  * Perform a full reset: clear the fixtures array, the journal (entries *and*
162
131
  * 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.
132
+ * job/queue state, the fine-tuning store (jobs, their append-only event logs,
133
+ * their poll counts, the create-time suffixes their model names are built from,
134
+ * and the module's monotonic clock), the ElevenLabs Voice Design store, the
135
+ * Gemini interaction/event-id counters, and any runtime chaos override
136
+ * (reverting to the construction-time chaos config), then re-zero the
137
+ * `aimock_fixtures_loaded` gauge.
166
138
  *
167
139
  * `targets` is `null` when no server is running (an in-process `reset()` before
168
140
  * `start()`). The process-global generation state is reset either way, since it
@@ -172,19 +144,27 @@ const JOURNAL_PARAMS = new Set([
172
144
  * `POST /__aimock/reset/fixtures` alias, and `LLMock.reset()`.
173
145
  */
174
146
  function performFullReset(fixtures, targets) {
175
- fixtures.length = 0;
147
+ clearFixtureQueue(fixtures);
176
148
  falJobs.clear();
177
149
  falQueueStates.clear();
150
+ clearBatchStore();
151
+ clearElevenLabsVoices();
152
+ clearFileStore();
153
+ clearFineTuningStore();
154
+ clearVectorStoreStore();
178
155
  resetInteractionCounter();
179
156
  resetEventIdCounter();
180
157
  if (!targets) return;
158
+ liveClosers.get(targets.defaults)?.();
181
159
  targets.journal.clear();
160
+ resetChaosWarnings(targets.defaults.logger);
182
161
  targets.defaults.chaos = void 0;
183
162
  targets.videoStates.clear();
184
163
  targets.openRouterVideoJobs.clear();
185
164
  targets.veoVideoJobs.clear();
186
165
  targets.grokVideoJobs.clear();
187
166
  targets.bytePlusVideoJobs.clear();
167
+ for (const { handler } of targets.mounts ?? []) handler.clearMcpFakes?.();
188
168
  if (targets.defaults.registry) targets.defaults.registry.setGauge("aimock_fixtures_loaded", {}, fixtures.length);
189
169
  }
190
170
  /**
@@ -230,22 +210,23 @@ function fixtureResponseKind(response) {
230
210
  return "unknown";
231
211
  }
232
212
  /**
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`.
213
+ * The testId a journal entry was scoped under — `resolveTestId` applied to the
214
+ * journaled headers and path, i.e. the SAME function that scoped the request
215
+ * (fixture match-counts, chaos) when it arrived. Node folds a repeated
216
+ * `X-Test-Id` into one string ("a, a") before either side sees it, and the
217
+ * request is counted and chaos-evaluated under that folded string, so the
218
+ * `?testId=` filter matches it verbatim too: no token-splitting here that the
219
+ * scoping path does not also do. (Splitting on "," additionally broke every
220
+ * legitimately comma-bearing id.) A raw `path.includes("testId=t1")` both
221
+ * prefix-collides with `t10` and matches unrelated params like `notTestId`.
222
+ *
223
+ * An MCP mount records the test id it resolved on the entry (I9, B3): a
224
+ * decoded header or query value, or the one bound to the session at
225
+ * `initialize`. That value wins; LLM entries carry none and are attributed as
226
+ * before.
239
227
  */
240
228
  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;
229
+ return entry.testId ?? resolveTestId(entry.headers, entry.path);
249
230
  }
250
231
  /**
251
232
  * The testId a chaos override is stored under, and read back with — resolved by
@@ -272,27 +253,198 @@ function writeBlankTestId(res) {
272
253
  res.end(JSON.stringify({ error: "Invalid 'X-Test-Id': header is present but empty" }));
273
254
  return true;
274
255
  }
275
- /** The chaos config in effect for one testId: its override, else the baseline. */
256
+ /**
257
+ * The chaos config in effect for one testId: its override, else the baseline —
258
+ * one or the other, never the two merged. Mirrors `resolveScopedDefaults` in
259
+ * chaos.ts, so what `GET /__aimock/chaos` reports for a scope is exactly what
260
+ * that scope's traffic is evaluated against.
261
+ */
276
262
  function effectiveChaos(defaults, scopeId) {
277
- const scoped = defaults.chaosByTestId?.get(scopeId);
278
- if (scoped) return scoped;
263
+ if (defaults.chaosByTestId?.has(scopeId)) return defaults.chaosByTestId.get(scopeId) ?? {};
279
264
  const current = defaults.chaos;
280
265
  if (!current) return {};
281
266
  return isChaosScope(current) ? current.base ?? {} : current;
282
267
  }
268
+ /** The `GET /__aimock/mcp/fakes` query-param vocabulary; anything else 400s. */
269
+ const MCP_FAKES_PARAMS = new Set([
270
+ "testId",
271
+ "context",
272
+ "mount"
273
+ ]);
274
+ /** A block's `mount` path, read without running getters; the add validates it. */
275
+ function controlFakeMountOf(raw) {
276
+ if (typeof raw !== "object" || raw === null) return MCP_FAKES_DEFAULT_MOUNT;
277
+ const value = Object.getOwnPropertyDescriptor(raw, "mount")?.value;
278
+ return typeof value === "string" ? value : MCP_FAKES_DEFAULT_MOUNT;
279
+ }
280
+ /**
281
+ * Check one `POST /__aimock/fixtures` `mcpFakes` value against the live
282
+ * mounts (F10, W6) without changing anything. Returns every error in input
283
+ * order (an empty list when the value can be added), and `apply`, which adds
284
+ * the blocks (auto-mounting an MCPMock where no mount serves a block's path)
285
+ * and returns the number of blocks added.
286
+ *
287
+ * The whole value is checked before any mount is touched, and the check
288
+ * never adds to a real mount: each group is added to a throwaway store,
289
+ * whose errors are the ones reported (it numbers the add `control-api#1`, as
290
+ * a new mount would; for an existing MCPMock the rejected group is re-run on
291
+ * its atomic add, so the errors name that mount's next `<n>`, I7). A group
292
+ * whose path no request to it can reach (a mount that is not an MCPMock
293
+ * serves it, an MCPMock answers it as its root, or it is under the control
294
+ * prefix) is a mount conflict (one error per block).
295
+ */
296
+ function planControlApiFakes(raw, mounts) {
297
+ const items = Array.isArray(raw) ? raw.map((block, position) => ({
298
+ position,
299
+ source: {
300
+ source: "control-api",
301
+ blockIndex: position,
302
+ raw: block
303
+ }
304
+ })) : [{
305
+ position: 0,
306
+ source: {
307
+ source: "control-api",
308
+ blockIndex: null,
309
+ raw
310
+ }
311
+ }];
312
+ const groups = /* @__PURE__ */ new Map();
313
+ if (items.length === 0) groups.set(MCP_FAKES_DEFAULT_MOUNT, {
314
+ path: MCP_FAKES_DEFAULT_MOUNT,
315
+ positions: [],
316
+ sources: []
317
+ });
318
+ for (const { position, source } of items) {
319
+ const path = controlFakeMountOf(source.raw);
320
+ const group = groups.get(path) ?? {
321
+ path,
322
+ positions: [],
323
+ sources: []
324
+ };
325
+ group.positions.push(position);
326
+ group.sources.push(source);
327
+ groups.set(path, group);
328
+ }
329
+ const found = [];
330
+ /** The paths `apply` auto-mounts, in order. */
331
+ const pending = [];
332
+ for (const group of groups.values()) {
333
+ const target = findFakeMount(mounts, group.path, pending);
334
+ if (target.kind === "conflict") {
335
+ group.positions.forEach((position) => {
336
+ const where = Array.isArray(raw) ? msg`mcpFakes[${position}]` : msg`mcpFakes`;
337
+ found.push({
338
+ position,
339
+ error: new FixtureLoadError({
340
+ rule: "mcp-fakes/mount-conflict",
341
+ file: null,
342
+ blockId: null,
343
+ entryId: null,
344
+ detail: msg`${where}: ${mountConflictDetail(group.path, target)}`
345
+ })
346
+ });
347
+ });
348
+ continue;
349
+ }
350
+ let failure = addFailure(() => new McpFakeStore().add(group.sources, { kind: "control-api" }));
351
+ if (target.kind === "none") pending.push(group.path);
352
+ if (!failure) continue;
353
+ if (target.kind === "mcp" && target.handler.addMcpFakes === MCPMock.prototype.addMcpFakes) {
354
+ const handler = target.handler;
355
+ failure = addFailure(() => handler.addMcpFakes?.(group.sources, { kind: "control-api" })) ?? failure;
356
+ }
357
+ for (const error of failure.errors) found.push({
358
+ position: positionOf(error, group),
359
+ error
360
+ });
361
+ }
362
+ found.sort((a, b) => a.position - b.position);
363
+ return {
364
+ errors: found.map((f) => f.error),
365
+ apply(wiring) {
366
+ let added = 0;
367
+ for (const group of groups.values()) {
368
+ const target = ensureFakeMount(mounts, group.path, {
369
+ journal: wiring.journal,
370
+ registry: wiring.defaults.registry,
371
+ logger: wiring.defaults.logger
372
+ }, wiring.defaults.logger);
373
+ if ("conflict" in target) continue;
374
+ const result = target.handler.addMcpFakes?.(group.sources, { kind: "control-api" });
375
+ for (const warning of result?.warnings ?? []) wiring.defaults.logger.warn(warning.message);
376
+ added += group.sources.length;
377
+ }
378
+ return added;
379
+ }
380
+ };
381
+ }
382
+ /** The `McpFakesAddError` that `add` throws, or `null` when it adds. */
383
+ function addFailure(add) {
384
+ try {
385
+ add();
386
+ return null;
387
+ } catch (err) {
388
+ if (err instanceof McpFakesAddError) return err;
389
+ throw err;
390
+ }
391
+ }
392
+ /**
393
+ * The request position of the block an add error names. The add numbers a
394
+ * run-time group's blocks by their place in the group (`[<j>]`), or gives no
395
+ * index to a single-object value; an error tied to no block sorts with the
396
+ * group's first block.
397
+ */
398
+ function positionOf(error, group) {
399
+ const first = group.positions[0] ?? 0;
400
+ if (error.blockId == null || error.file === null) return first;
401
+ const at = group.positions.findIndex((_, j) => error.blockId === blockIdOf(error.file ?? "", "", j));
402
+ return at === -1 ? first : group.positions[at];
403
+ }
404
+ function hasFakesSnapshot(handler) {
405
+ return "fakesSnapshot" in handler && typeof handler.fakesSnapshot === "function";
406
+ }
407
+ /**
408
+ * `GET /__aimock/mcp/fakes` (spec 6.5, I9): for each MCP mount (or the one
409
+ * named by `mount`), the blocks that apply to the requested test id and
410
+ * context, with every entry id and its consumed state for that test id.
411
+ * A mount is listed when it takes fakes (it implements `addMcpFakes`, the
412
+ * check every other fakes path uses); one with no `fakesSnapshot` lists no
413
+ * blocks.
414
+ */
415
+ function listControlApiFakes(mounts, testId, context, mount) {
416
+ const listed = mounts.flatMap(({ path, handler }) => isFakeMount(handler) && (mount === null || path === mount) ? [{
417
+ mount: path,
418
+ blocks: hasFakesSnapshot(handler) ? handler.fakesSnapshot(testId, context) : []
419
+ }] : []);
420
+ return mount !== null && listed.length === 0 ? null : listed;
421
+ }
283
422
  /**
284
423
  * Handle requests under `/__aimock/`. Returns `true` if the request was
285
424
  * handled, `false` if the path doesn't match the control prefix.
286
425
  */
287
- async function handleControlAPI(req, res, pathname, searchParams, fixtures, journal, videoStates, openRouterVideoJobs, veoVideoJobs, grokVideoJobs, bytePlusVideoJobs, defaults) {
426
+ async function handleControlAPI(req, res, pathname, searchParams, fixtures, journal, videoStates, openRouterVideoJobs, veoVideoJobs, grokVideoJobs, bytePlusVideoJobs, defaults, mounts) {
288
427
  if (!pathname.startsWith(CONTROL_PREFIX)) return false;
289
- const subPath = pathname.slice(9);
428
+ const subPath = pathname.slice(CONTROL_PREFIX.length);
290
429
  setCorsHeaders(res);
291
430
  if (subPath === "/health" && req.method === "GET") {
292
431
  res.writeHead(200, { "Content-Type": "application/json" });
293
432
  res.end(JSON.stringify({ status: "ok" }));
294
433
  return true;
295
434
  }
435
+ if (subPath === "/openapi.json" && req.method === "GET") {
436
+ res.writeHead(200, { "Content-Type": "application/json" });
437
+ res.end(JSON.stringify(buildOpenApiDocument()));
438
+ return true;
439
+ }
440
+ if (subPath === "/routes" && req.method === "GET") {
441
+ res.writeHead(200, { "Content-Type": "application/json" });
442
+ res.end(JSON.stringify({
443
+ count: CATALOG_ROUTES.length,
444
+ routes: CATALOG_ROUTES
445
+ }));
446
+ return true;
447
+ }
296
448
  if (subPath === "/journal" && req.method === "GET") {
297
449
  const bad = (message) => {
298
450
  res.writeHead(400, { "Content-Type": "application/json" });
@@ -320,6 +472,7 @@ async function handleControlAPI(req, res, pathname, searchParams, fixtures, jour
320
472
  const methodFilter = searchParams.get("method");
321
473
  const serviceFilter = searchParams.get("service");
322
474
  const testIdFilter = searchParams.get("testId");
475
+ const requestIdFilter = searchParams.get("requestId");
323
476
  let entries = journal.getAll();
324
477
  if (methodFilter !== null) {
325
478
  const want = methodFilter.toUpperCase();
@@ -329,6 +482,7 @@ async function handleControlAPI(req, res, pathname, searchParams, fixtures, jour
329
482
  if (statusFilter !== void 0) entries = entries.filter((e) => e.response.status === statusFilter);
330
483
  if (serviceFilter !== null) entries = entries.filter((e) => e.service === serviceFilter);
331
484
  if (testIdFilter !== null) entries = entries.filter((e) => journalEntryTestId(e) === testIdFilter);
485
+ if (requestIdFilter !== null) entries = entries.filter((e) => e.headers["x-request-id"] === requestIdFilter);
332
486
  const total = entries.length;
333
487
  if (offset > 0) entries = entries.slice(offset);
334
488
  if (limit >= 0) entries = entries.slice(0, limit);
@@ -362,7 +516,7 @@ async function handleControlAPI(req, res, pathname, searchParams, fixtures, jour
362
516
  fixtures: fixtures.map((fixture, index) => ({
363
517
  index,
364
518
  match: redactForInspection(fixture.match),
365
- responseKind: fixtureResponseKind(fixture.response),
519
+ responseKind: isOneShotError(fixture) ? "error" : fixtureResponseKind(fixture.response),
366
520
  ...fixture.latency !== void 0 ? { latency: fixture.latency } : {},
367
521
  ...fixture.chaos !== void 0 ? { chaos: fixture.chaos } : {}
368
522
  }))
@@ -412,32 +566,53 @@ async function handleControlAPI(req, res, pathname, searchParams, fixtures, jour
412
566
  res.end(JSON.stringify({ error: "Invalid body: expected a JSON object" }));
413
567
  return true;
414
568
  }
415
- const allowed = [
416
- "dropRate",
417
- "malformedRate",
418
- "disconnectRate"
419
- ];
420
- for (const key of Object.keys(parsed)) if (!allowed.includes(key)) {
569
+ for (const key of Object.keys(parsed)) if (!CHAOS_FIELD_NAMES.includes(key)) {
421
570
  res.writeHead(400, { "Content-Type": "application/json" });
422
571
  res.end(JSON.stringify({ error: `Unknown chaos field: '${key}'` }));
423
572
  return true;
424
573
  }
425
574
  const next = {};
426
- for (const key of allowed) {
575
+ for (const key of CHAOS_FIELD_NAMES) {
427
576
  const value = parsed[key];
428
577
  if (value === void 0) continue;
429
- if (typeof value !== "number" || Number.isNaN(value) || value < 0 || value > 1) {
578
+ const accepted = typeof value === "number" ? parseChaosField(key, value) : void 0;
579
+ if (accepted === void 0) {
580
+ const shape = CHAOS_FIELDS[key].integer ? "a whole number of ms" : "a number";
430
581
  res.writeHead(400, { "Content-Type": "application/json" });
431
- res.end(JSON.stringify({ error: `Invalid '${key}': must be a number between 0 and 1` }));
582
+ res.end(JSON.stringify({ error: `Invalid '${key}': must be ${shape} between 0 and ${CHAOS_FIELDS[key].max}` }));
432
583
  return true;
433
584
  }
434
- next[key] = value;
585
+ next[key] = accepted;
586
+ }
587
+ const shadowed = effectiveChaos(defaults, scopeId);
588
+ const droppedFields = Object.keys(shadowed).filter((key) => shadowed[key] !== void 0 && next[key] === void 0);
589
+ if (droppedFields.length > 0) {
590
+ const lost = droppedFields.map((key) => `${key}=${shadowed[key]}`).join(", ");
591
+ 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
592
  }
436
593
  defaults.chaosByTestId?.set(scopeId, next);
437
594
  res.writeHead(200, { "Content-Type": "application/json" });
438
595
  res.end(JSON.stringify({ chaos: effectiveChaos(defaults, scopeId) }));
439
596
  return true;
440
597
  }
598
+ if (subPath === "/mcp/fakes" && req.method === "GET") {
599
+ const reply = (status, body) => {
600
+ res.writeHead(status, { "Content-Type": "application/json" });
601
+ res.end(JSON.stringify(body));
602
+ return true;
603
+ };
604
+ for (const key of searchParams.keys()) if (!MCP_FAKES_PARAMS.has(key)) return reply(400, { error: `Unknown query parameter: '${key}'. Supported: ${[...MCP_FAKES_PARAMS].join(", ")}` });
605
+ const testId = searchParams.get("testId") || null;
606
+ const context = searchParams.get("context") || null;
607
+ const mount = searchParams.get("mount") || null;
608
+ const listed = listControlApiFakes(mounts, testId, context, mount);
609
+ if (listed === null) return reply(404, { error: build(msg`No MCP mount at ${quote(mount ?? "")}`) });
610
+ return reply(200, {
611
+ testId,
612
+ context,
613
+ mounts: listed
614
+ });
615
+ }
441
616
  if (subPath === "/fixtures" && req.method === "POST") {
442
617
  let raw;
443
618
  try {
@@ -459,29 +634,60 @@ async function handleControlAPI(req, res, pathname, searchParams, fixtures, jour
459
634
  res.end(JSON.stringify({ error: `Invalid JSON: ${msg}` }));
460
635
  return true;
461
636
  }
462
- if (!Array.isArray(parsed.fixtures)) {
637
+ const withFakes = typeof parsed === "object" && parsed !== null && Object.hasOwn(parsed, "mcpFakes");
638
+ if (parsed === null || !Array.isArray(parsed.fixtures) && !(withFakes && parsed.fixtures === void 0)) {
463
639
  res.writeHead(400, { "Content-Type": "application/json" });
464
640
  res.end(JSON.stringify({ error: "Missing or invalid \"fixtures\" array" }));
465
641
  return true;
466
642
  }
467
- const converted = parsed.fixtures.map((e) => entryToFixture(e));
643
+ const entries = Array.isArray(parsed.fixtures) ? parsed.fixtures : [];
644
+ const missingMatchIndex = entries.findIndex((entry) => entry !== null && typeof entry === "object" && !Array.isArray(entry) && entry.match == null);
645
+ if (missingMatchIndex !== -1) {
646
+ res.writeHead(400, { "Content-Type": "application/json" });
647
+ res.end(JSON.stringify({ error: `Fixture at index ${missingMatchIndex} is missing match` }));
648
+ return true;
649
+ }
650
+ const invalidMatchIndex = entries.findIndex((entry) => entry !== null && typeof entry === "object" && !Array.isArray(entry) && entry.match != null && (typeof entry.match !== "object" || Array.isArray(entry.match)));
651
+ if (invalidMatchIndex !== -1) {
652
+ res.writeHead(400, { "Content-Type": "application/json" });
653
+ res.end(JSON.stringify({ error: `Fixture at index ${invalidMatchIndex}: match must be an object` }));
654
+ return true;
655
+ }
656
+ const converted = entries.map((e) => entryToFixture(e));
468
657
  const errors = validateFixtures(converted).filter((i) => i.severity === "error");
469
- if (errors.length > 0) {
658
+ const fakes = withFakes ? planControlApiFakes(parsed.mcpFakes, mounts) : null;
659
+ const fakeErrors = fakes?.errors ?? [];
660
+ const validationFailed = (details) => {
470
661
  res.writeHead(400, { "Content-Type": "application/json" });
471
662
  res.end(JSON.stringify({
472
663
  error: "Validation failed",
473
- details: errors
664
+ details
474
665
  }));
475
666
  return true;
667
+ };
668
+ if (errors.length > 0 || fakeErrors.length > 0) return validationFailed([...errors, ...fakeErrors.map((e) => e.toJSON())]);
669
+ let mcpFakesAdded = 0;
670
+ if (fakes) try {
671
+ mcpFakesAdded = fakes.apply({
672
+ journal,
673
+ defaults
674
+ });
675
+ } catch (err) {
676
+ if (!(err instanceof McpFakesAddError)) throw err;
677
+ return validationFailed(err.errors.map((e) => e.toJSON()));
476
678
  }
477
679
  fixtures.push(...converted);
478
680
  if (defaults.registry) defaults.registry.setGauge("aimock_fixtures_loaded", {}, fixtures.length);
479
681
  res.writeHead(200, { "Content-Type": "application/json" });
480
- res.end(JSON.stringify({ added: converted.length }));
682
+ res.end(JSON.stringify(withFakes ? {
683
+ added: converted.length,
684
+ mcpFakesAdded
685
+ } : { added: converted.length }));
481
686
  return true;
482
687
  }
483
688
  if (subPath === "/fixtures" && req.method === "DELETE") {
484
- fixtures.length = 0;
689
+ clearFixtureQueue(fixtures);
690
+ for (const { handler } of mounts) handler.clearMcpFakes?.();
485
691
  if (defaults.registry) defaults.registry.setGauge("aimock_fixtures_loaded", {}, fixtures.length);
486
692
  res.writeHead(200, { "Content-Type": "application/json" });
487
693
  res.end(JSON.stringify({ cleared: true }));
@@ -494,7 +700,8 @@ async function handleControlAPI(req, res, pathname, searchParams, fixtures, jour
494
700
  veoVideoJobs,
495
701
  grokVideoJobs,
496
702
  bytePlusVideoJobs,
497
- defaults
703
+ defaults,
704
+ mounts
498
705
  });
499
706
  if (subPath === "/reset" && req.method === "POST") {
500
707
  performFullReset(fixtures, resetTargets());
@@ -575,32 +782,7 @@ async function handleControlAPI(req, res, pathname, searchParams, fixtures, jour
575
782
  }
576
783
  }
577
784
  }
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
- };
785
+ queueOneShotError(fixtures, status, errorBody ?? void 0);
604
786
  res.writeHead(200, { "Content-Type": "application/json" });
605
787
  res.end(JSON.stringify({ queued: true }));
606
788
  return true;
@@ -608,6 +790,19 @@ async function handleControlAPI(req, res, pathname, searchParams, fixtures, jour
608
790
  handleNotFound(res, `Unknown control endpoint: ${pathname}`);
609
791
  return true;
610
792
  }
793
+ /**
794
+ * The client tore the connection down before its request body had fully
795
+ * arrived. Node rejects the pending body read with `aborted` (ECONNRESET) and
796
+ * destroys the response, so the rejection surfaces in the handler's read arm
797
+ * looking like any other thrown error. It is caller behaviour, not a server
798
+ * fault: no status can reach the client, so none must be journaled as
799
+ * delivered. A body that blew its size cap also leaves the request destroyed
800
+ * and incomplete, but that one is aimock's own decision, not a peer abort.
801
+ */
802
+ function clientAbortedMidBody(req, res, err) {
803
+ if (err instanceof RequestBodyTooLargeError) return false;
804
+ return res.destroyed && !req.complete;
805
+ }
611
806
  async function handleCompletions(req, res, fixtures, journal, defaults, modelFallback, providerKey, openRouter = false) {
612
807
  setCorsHeaders(res);
613
808
  let raw;
@@ -615,19 +810,43 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
615
810
  raw = await readBody(req);
616
811
  } catch (err) {
617
812
  const msg = err instanceof Error ? err.message : "Failed to read request body";
813
+ const clientFault = err instanceof RequestBodyTooLargeError;
814
+ const route = `${req.method ?? "POST"} ${req.url ?? COMPLETIONS_PATH}`;
815
+ const clientAbort = clientAbortedMidBody(req, res, err);
816
+ const bodyLimitDisconnect = clientFault && (req.destroyed || res.destroyed);
817
+ if (clientAbort || bodyLimitDisconnect) {
818
+ const interruptReason = bodyLimitDisconnect ? "request body exceeded size limit" : "client aborted";
819
+ defaults.logger.warn(bodyLimitDisconnect ? `${route}: ${msg}` : `${route}: client aborted mid-body (${msg})`);
820
+ journal.add({
821
+ method: req.method ?? "POST",
822
+ path: req.url ?? COMPLETIONS_PATH,
823
+ headers: flattenHeaders(req.headers),
824
+ body: null,
825
+ response: {
826
+ status: 0,
827
+ fixture: null,
828
+ interrupted: true,
829
+ interruptReason
830
+ }
831
+ });
832
+ return;
833
+ }
834
+ const status = clientFault ? 400 : 500;
835
+ if (clientFault) defaults.logger.warn(`${route}: ${msg}`);
836
+ else defaults.logger.error(`${route}: ${msg}`);
618
837
  journal.add({
619
838
  method: req.method ?? "POST",
620
839
  path: req.url ?? COMPLETIONS_PATH,
621
840
  headers: flattenHeaders(req.headers),
622
841
  body: null,
623
842
  response: {
624
- status: 500,
843
+ status,
625
844
  fixture: null
626
845
  }
627
846
  });
628
- writeErrorResponse(res, 500, openRouter ? serializeOpenRouterError(500, `Request body read failed: ${msg}`) : JSON.stringify({ error: {
847
+ writeErrorResponse(res, status, openRouter ? serializeOpenRouterError(status, `Request body read failed: ${msg}`) : JSON.stringify({ error: {
629
848
  message: `Request body read failed: ${msg}`,
630
- type: "server_error"
849
+ type: clientFault ? "invalid_request_error" : "server_error"
631
850
  } }));
632
851
  return;
633
852
  }
@@ -691,6 +910,51 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
691
910
  } }));
692
911
  return;
693
912
  }
913
+ const nullMessageIndex = body.messages.findIndex((message) => message === null);
914
+ if (nullMessageIndex !== -1) {
915
+ const message = `Invalid request: messages[${nullMessageIndex}] must be an object`;
916
+ journal.add({
917
+ method: req.method ?? "POST",
918
+ path: req.url ?? COMPLETIONS_PATH,
919
+ headers: flattenHeaders(req.headers),
920
+ body: null,
921
+ response: {
922
+ status: 400,
923
+ fixture: null
924
+ }
925
+ });
926
+ writeErrorResponse(res, 400, openRouter ? serializeOpenRouterError(400, message) : JSON.stringify({ error: {
927
+ message,
928
+ type: "invalid_request_error",
929
+ param: null,
930
+ code: null
931
+ } }));
932
+ return;
933
+ }
934
+ if (new URL(req.url ?? COMPLETIONS_PATH, "http://localhost").pathname === COMPLETIONS_PATH) {
935
+ let toolsError = validateToolsField(body.tools);
936
+ if (!toolsError && Array.isArray(body.tools)) {
937
+ const nullFunctionIndex = body.tools.findIndex((tool) => tool?.function === null);
938
+ if (nullFunctionIndex !== -1) toolsError = `tools[${nullFunctionIndex}].function must not be null`;
939
+ }
940
+ if (toolsError) {
941
+ journal.add({
942
+ method: req.method ?? "POST",
943
+ path: req.url ?? COMPLETIONS_PATH,
944
+ headers: flattenHeaders(req.headers),
945
+ body: null,
946
+ response: {
947
+ status: 400,
948
+ fixture: null
949
+ }
950
+ });
951
+ writeErrorResponse(res, 400, JSON.stringify({ error: {
952
+ message: toolsError,
953
+ type: "invalid_request_error"
954
+ } }));
955
+ return;
956
+ }
957
+ }
694
958
  const method = req.method ?? "POST";
695
959
  const path = req.url ?? COMPLETIONS_PATH;
696
960
  const flatHeaders = flattenHeaders(req.headers);
@@ -703,7 +967,15 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
703
967
  let preResolvedResponse = null;
704
968
  let responseModel = body.model;
705
969
  let fixtureCountIncremented = false;
706
- if (openRouter && Array.isArray(body.models) && body.models.length > 0) {
970
+ const openRouterFallback = openRouter && Array.isArray(body.models) && body.models.length > 0;
971
+ const selectFixture = (probe, counts) => {
972
+ for (;;) {
973
+ const attempt = matchFixtureDiagnostic(fixtures, probe, counts, defaults.requestTransform, matchOptions);
974
+ if (attempt.fixture && isOneShotError(attempt.fixture) && !claimOneShotError(fixtures, attempt.fixture)) continue;
975
+ return attempt;
976
+ }
977
+ };
978
+ if (openRouterFallback) {
707
979
  const candidates = buildOpenRouterCandidates(body);
708
980
  let matchCounts = journal.getFixtureMatchCountsForTest(testId);
709
981
  const allowFallbacks = body.provider?.allow_fallbacks !== false;
@@ -714,7 +986,7 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
714
986
  ...body,
715
987
  model: candidate
716
988
  };
717
- const attempt = matchFixtureDiagnostic(fixtures, probe, matchCounts, defaults.requestTransform, matchOptions);
989
+ const attempt = selectFixture(probe, matchCounts);
718
990
  skippedBySequenceOrTurn = Math.max(skippedBySequenceOrTurn, attempt.skippedBySequenceOrTurn);
719
991
  if (!attempt.fixture) return null;
720
992
  const response = await resolveResponse(attempt.fixture, probe);
@@ -751,35 +1023,48 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
751
1023
  preResolvedResponse = lastErrorResponse;
752
1024
  }
753
1025
  } else {
754
- const single = matchFixtureDiagnostic(fixtures, body, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform, matchOptions);
1026
+ const single = selectFixture(body, journal.getFixtureMatchCountsForTest(testId));
755
1027
  fixture = single.fixture;
756
1028
  skippedBySequenceOrTurn = single.skippedBySequenceOrTurn;
757
1029
  }
758
1030
  if (fixture) {
759
1031
  if (!fixtureCountIncremented) journal.incrementFixtureMatchCount(fixture, fixtures, testId);
760
- defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);
1032
+ defaults.logger.debug(`Fixture matched: ${describeMatch(fixture.match, fixtures.indexOf(fixture))}`);
761
1033
  } else {
762
1034
  const lastUserMsg = body.messages.filter((m) => m.role === "user").pop();
763
1035
  const snippet = typeof lastUserMsg?.content === "string" ? lastUserMsg.content.slice(0, 80) : "";
764
1036
  defaults.logger.debug(`No fixture matched for request (model=${body.model ?? "?"}, msg="${snippet}")`);
765
1037
  }
766
- const chaosAction = evaluateChaos(fixture, defaults.chaos, req.headers, defaults.logger, req.url);
1038
+ const chaosConfig = resolveChaosConfig(fixture, defaults.chaos, req.headers, defaults.logger, req.url);
1039
+ await awaitChaosLatency(fixture, defaults.chaos, req.headers, defaults.logger, req.url, res, chaosConfig);
1040
+ const goneAfterLatency = responseGoneReason(res);
1041
+ if (goneAfterLatency !== null) {
1042
+ releaseOneShotError(fixtures, fixture);
1043
+ defaults.logger.debug(`[chaos] ${method} ${path}: ${describeUnwritableReason(goneAfterLatency)} after the latency delay — not served, not journalled`);
1044
+ return;
1045
+ }
1046
+ const chaosAction = evaluateChaos(fixture, defaults.chaos, req.headers, defaults.logger, req.url, chaosConfig);
767
1047
  const chaosContext = {
768
1048
  method,
769
1049
  path,
770
1050
  headers: flatHeaders,
771
1051
  body
772
1052
  };
773
- if (chaosAction === "drop" || chaosAction === "disconnect") {
774
- applyChaosAction(chaosAction, res, fixture, journal, chaosContext, fixture ? "fixture" : "proxy", defaults.registry);
1053
+ const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);
1054
+ const missWouldProxy = wouldProxyMiss(effectiveStrict, defaults.record, providerKey);
1055
+ const noFixtureSource = missWouldProxy ? "proxy" : "internal";
1056
+ if (chaosAction === "drop" || chaosAction === "disconnect" || chaosAction === "rateLimit") {
1057
+ applyChaosAction(chaosAction, res, fixture, journal, chaosContext, fixture ? "fixture" : noFixtureSource, defaults.registry, defaults.logger);
1058
+ releaseOneShotError(fixtures, fixture);
775
1059
  return;
776
1060
  }
777
- if (fixture && chaosAction === "malformed") {
778
- applyChaosAction(chaosAction, res, fixture, journal, chaosContext, "fixture", defaults.registry);
1061
+ if (chaosAction === "malformed" && (fixture || !missWouldProxy)) {
1062
+ applyChaosAction(chaosAction, res, fixture, journal, chaosContext, fixture ? "fixture" : "internal", defaults.registry, defaults.logger);
1063
+ releaseOneShotError(fixtures, fixture);
779
1064
  return;
780
1065
  }
781
1066
  if (!fixture) {
782
- if (resolveStrictMode(defaults.strict, req.headers)) {
1067
+ if (effectiveStrict) {
783
1068
  const strictStatus = 503;
784
1069
  const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);
785
1070
  defaults.logger.error(strictNoMatchLogLine(req.method ?? "POST", req.url ?? COMPLETIONS_PATH, skippedBySequenceOrTurn));
@@ -805,7 +1090,7 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
805
1090
  if (defaults.record && providerKey) {
806
1091
  const hookOptions = chaosAction === "malformed" ? {
807
1092
  beforeWriteResponse: () => {
808
- applyChaosAction(chaosAction, res, null, journal, chaosContext, "proxy", defaults.registry);
1093
+ applyChaosAction(chaosAction, res, null, journal, chaosContext, "proxy", defaults.registry, defaults.logger);
809
1094
  return true;
810
1095
  },
811
1096
  onHookBypassed: (reason) => {
@@ -817,9 +1102,22 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
817
1102
  });
818
1103
  }
819
1104
  } : void 0;
820
- const outcome = await proxyAndRecord(req, res, body, providerKey, req.url ?? COMPLETIONS_PATH, fixtures, defaults, raw, hookOptions);
1105
+ let clientHungUp = false;
1106
+ const noteClientHangUp = () => {
1107
+ if (!res.writableEnded) clientHungUp = true;
1108
+ };
1109
+ req.socket?.on("end", noteClientHangUp);
1110
+ req.socket?.on("error", noteClientHangUp);
1111
+ let outcome;
1112
+ try {
1113
+ outcome = await proxyAndRecord(req, res, body, providerKey, req.url ?? COMPLETIONS_PATH, fixtures, defaults, raw, hookOptions);
1114
+ } finally {
1115
+ req.socket?.off("end", noteClientHangUp);
1116
+ req.socket?.off("error", noteClientHangUp);
1117
+ }
821
1118
  if (outcome === "handled_by_hook") return;
822
1119
  if (outcome !== "not_configured") {
1120
+ const destroyedMidStream = res.destroyed && !res.writableEnded;
823
1121
  journal.add({
824
1122
  method: req.method ?? "POST",
825
1123
  path: req.url ?? COMPLETIONS_PATH,
@@ -828,11 +1126,19 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
828
1126
  response: {
829
1127
  status: res.statusCode ?? 200,
830
1128
  fixture: null,
831
- source: "proxy"
1129
+ source: "proxy",
1130
+ ...destroyedMidStream ? {
1131
+ interrupted: true,
1132
+ interruptReason: clientHungUp ? "client aborted" : "proxy stream destroyed"
1133
+ } : {}
832
1134
  }
833
1135
  });
834
1136
  return;
835
1137
  }
1138
+ if (chaosAction === "malformed") {
1139
+ applyChaosAction(chaosAction, res, null, journal, chaosContext, "internal", defaults.registry, defaults.logger);
1140
+ return;
1141
+ }
836
1142
  }
837
1143
  journal.add({
838
1144
  method: req.method ?? "POST",
@@ -908,7 +1214,7 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
908
1214
  const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);
909
1215
  const effReasoning = resolveReasoningForModel(response.reasoning, responseModel, effectiveStrict, defaults.logger);
910
1216
  const streaming = body.stream === true;
911
- const streamingBlocks = streaming && response.blocks && response.blocks.length > 0 ? resolveFixtureBlocks(response.blocks) : void 0;
1217
+ const blockOutcome = response.blocks && response.blocks.length > 0 ? resolveFixtureBlockOutcome(response.blocks) : void 0;
912
1218
  const journalEntry = journal.add({
913
1219
  method: req.method ?? "POST",
914
1220
  path: req.url ?? COMPLETIONS_PATH,
@@ -920,12 +1226,15 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
920
1226
  }
921
1227
  });
922
1228
  if (!streaming) {
923
- const completion = buildContentWithToolCallsCompletion(response.content ?? "", response.toolCalls ?? [], responseModel, effReasoning, overrides, body.messages);
1229
+ const completion = buildContentWithToolCallsCompletion(blockOutcome?.content ?? response.content ?? "", blockOutcome?.toolCalls ?? response.toolCalls ?? [], responseModel, effReasoning, blockOutcome ? {
1230
+ ...overrides,
1231
+ finishReason: overrides?.finishReason ?? (blockOutcome.hasToolCalls ? "tool_calls" : "stop")
1232
+ } : overrides, body.messages);
924
1233
  res.writeHead(200, { "Content-Type": "application/json" });
925
1234
  res.end(JSON.stringify(shapeORCompletion(completion, overrides)));
926
1235
  } 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("");
1236
+ const chunks = buildContentWithToolCallsChunks(blockOutcome?.content ?? response.content ?? "", blockOutcome?.toolCalls ?? response.toolCalls ?? [], responseModel, chunkSize, effReasoning, overrides, blockOutcome?.ordered);
1237
+ 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
1238
  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
1239
  shapeORChunks(chunks, usageChunk, overrides);
931
1240
  const interruption = createInterruptionSignal(fixture);
@@ -1055,9 +1364,24 @@ async function createServer(fixtures, options, mounts, serviceFixtures) {
1055
1364
  }
1056
1365
  /** @internal Config startup uses this to avoid parsing a selected auth source twice. */
1057
1366
  async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mounts, serviceFixtures) {
1367
+ const logger = new Logger(options?.logLevel ?? "silent");
1368
+ const mountList = mounts ?? [];
1369
+ const handOff = serviceFixtures?.mcpFakes && serviceFixtures.mcpFakes.length > 0 ? handOffMcpFakes(mountList, serviceFixtures.mcpFakes, logger) : null;
1370
+ try {
1371
+ return await startServer(fixtures, options, resolvedAuth, mountList, logger, serviceFixtures, () => handOff?.commit());
1372
+ } catch (err) {
1373
+ handOff?.undo();
1374
+ throw err;
1375
+ }
1376
+ }
1377
+ /**
1378
+ * The server of `createServerWithResolvedAuth`, after the MCP fakes hand-off.
1379
+ * `commitHandOff` runs once the server listens; if it throws, the server is
1380
+ * closed and start rejects.
1381
+ */
1382
+ async function startServer(fixtures, options, resolvedAuth, mountList, logger, serviceFixtures, commitHandOff) {
1058
1383
  const host = options?.host ?? "127.0.0.1";
1059
1384
  const port = options?.port ?? 0;
1060
- const logger = new Logger(options?.logLevel ?? "silent");
1061
1385
  const registry = options?.metrics ? createMetricsRegistry() : void 0;
1062
1386
  const serverOptions = options ?? {};
1063
1387
  const chaosByTestId = /* @__PURE__ */ new Map();
@@ -1109,21 +1433,14 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1109
1433
  }
1110
1434
  };
1111
1435
  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`);
1436
+ const chaosDefaults = options.chaos;
1437
+ for (const field of CHAOS_FIELD_NAMES) {
1438
+ const value = chaosDefaults[field];
1439
+ if (value === void 0) continue;
1440
+ if (typeof value === "number" && parseChaosField(field, value) !== void 0) continue;
1441
+ const shape = CHAOS_FIELDS[field].integer ? "a whole number of ms" : "a number";
1442
+ 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`);
1443
+ }
1127
1444
  }
1128
1445
  for (const { name, config } of [
1129
1446
  {
@@ -1169,67 +1486,276 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1169
1486
  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
1487
  }
1171
1488
  }
1489
+ /**
1490
+ * The in-flight request, scoped to the async execution that serves it.
1491
+ * Entered once in the `http.createServer` callback, so every handler —
1492
+ * including the ones in other modules that only ever see the shared
1493
+ * `journal` — runs inside it.
1494
+ */
1495
+ const requestScope = new AsyncLocalStorage();
1496
+ /** The journal entry each request produced, keyed by the request object. */
1497
+ const ownJournalEntry = /* @__PURE__ */ new WeakMap();
1172
1498
  const journal = new Journal({
1173
1499
  maxEntries: options?.journalMaxEntries ?? 1e3,
1174
- fixtureCountsMaxTestIds: options?.fixtureCountsMaxTestIds ?? 500
1500
+ fixtureCountsMaxTestIds: options?.fixtureCountsMaxTestIds ?? 500,
1501
+ onAdd: (entry) => {
1502
+ const req = requestScope.getStore();
1503
+ if (req) ownJournalEntry.set(req, entry);
1504
+ }
1175
1505
  });
1176
1506
  const videoStates = new VideoStateMap();
1177
1507
  const openRouterVideoJobs = new OpenRouterVideoJobMap();
1178
1508
  const veoVideoJobs = new VeoVideoJobMap();
1179
1509
  const grokVideoJobs = new GrokVideoJobMap();
1180
1510
  const bytePlusVideoJobs = new BytePlusVideoJobMap();
1181
- if (mounts) for (const { handler } of mounts) {
1511
+ /**
1512
+ * The OpenAI error `type` that goes with a status: 4xx is the caller's
1513
+ * fault, everything else is ours. Shared by `routeError` and the envelopes
1514
+ * it calls so the body can never disagree with the status line.
1515
+ */
1516
+ function errorTypeForStatus(status) {
1517
+ return status >= 400 && status < 500 ? "invalid_request_error" : "server_error";
1518
+ }
1519
+ /**
1520
+ * The newest journal entry this request already produced, or null.
1521
+ *
1522
+ * Attribution is by the identity of the `IncomingMessage` the entry was
1523
+ * written under — never by `x-request-id`. That header is CALLER-supplied
1524
+ * (`resolveRequestId` echoes a well-formed one) and nothing makes it
1525
+ * unique: a retry, a harness that pins one id, or a load generator sends
1526
+ * the same id on many requests, and keying off it made a crash rewrite an
1527
+ * EARLIER request's entry while leaving the crashing request untraced. The
1528
+ * request object cannot be collided by a caller, so one entry belongs to
1529
+ * exactly one request. A handler writes at most one entry; if it writes
1530
+ * more, the newest wins, which is the entry the old scan would have found.
1531
+ */
1532
+ function lastJournalEntryFor(req) {
1533
+ return ownJournalEntry.get(req) ?? null;
1534
+ }
1535
+ /**
1536
+ * Uniform terminal arm for a route handler that threw. Every route error
1537
+ * path logs the route, journals the failure, sets CORS and answers the error
1538
+ * envelope — so a handler crash is never invisible in the logs or in
1539
+ * `/__aimock/journal`.
1540
+ *
1541
+ * ONE request produces ONE entry. `res.headersSent` is not the test for that:
1542
+ * a handler that journaled its 200 and then threw before `writeHead` has
1543
+ * sent no headers, and journaling again would leave two contradicting
1544
+ * entries for one request. So the existing entry is looked up by request
1545
+ * identity ({@link lastJournalEntryFor}) and AMENDED — status to what the
1546
+ * client actually got when the response is still changeable, or interrupted
1547
+ * when it isn't — and a fresh entry is
1548
+ * added only when the handler journaled nothing. That holds after the
1549
+ * response too: a handler that answered and then crashed before its own
1550
+ * journal write gets an entry carrying the status already sent, so a
1551
+ * post-response crash is never invisible.
1552
+ *
1553
+ * Once headers are on the wire the status can't change, so the socket is
1554
+ * destroyed: a clean `end()` reads as EOF and the client treats a truncated
1555
+ * stream as a complete response. The chat-style SSE routes whose clients
1556
+ * surface an error frame pass `streamEvent`; that frame is written first,
1557
+ * but the tear-down still follows it.
1558
+ *
1559
+ * A body that blew its size cap is the CALLER's fault, not a server fault:
1560
+ * it logs at warn, but if the reader destroyed the socket it journals an
1561
+ * interruption with status 0 because no 4xx reached the client. A client
1562
+ * that hung up before its body arrived
1563
+ * ({@link clientAbortedMidBody}) is not a fault at all: it logs at warn and
1564
+ * journals a torn socket (`interrupted`, status 0), never a 500 the client
1565
+ * could not have received.
1566
+ */
1567
+ function routeError(req, res, err, pathname, opts) {
1568
+ const route = `${req.method ?? "?"} ${pathname}`;
1569
+ const msg = err instanceof Error ? err.message : "Internal error";
1570
+ const clientFault = err instanceof RequestBodyTooLargeError;
1571
+ const clientAbort = clientAbortedMidBody(req, res, err);
1572
+ const status = clientFault ? 400 : 500;
1573
+ if (clientAbort) logger.warn(`${route}: client aborted mid-body (${msg})`);
1574
+ else if (clientFault) logger.warn(`${route}: ${msg}`);
1575
+ else logger.error(`${route}: ${msg}`);
1576
+ if (err instanceof Error && err.stack) logger.debug(err.stack);
1577
+ const existing = lastJournalEntryFor(req);
1578
+ const bodyLimitDisconnect = clientFault && (req.destroyed || res.destroyed);
1579
+ if ((clientAbort || bodyLimitDisconnect) && !res.headersSent) {
1580
+ const interruptReason = bodyLimitDisconnect ? "request body exceeded size limit" : "client aborted";
1581
+ if (existing) {
1582
+ if (!existing.response.interrupted) {
1583
+ existing.response.status = 0;
1584
+ existing.response.interrupted = true;
1585
+ existing.response.interruptReason = interruptReason;
1586
+ }
1587
+ return;
1588
+ }
1589
+ try {
1590
+ journal.add({
1591
+ method: req.method ?? "?",
1592
+ path: req.url ?? "?",
1593
+ headers: flattenHeaders(req.headers),
1594
+ body: null,
1595
+ ...opts?.service ? { service: opts.service } : {},
1596
+ response: {
1597
+ status: 0,
1598
+ fixture: null,
1599
+ source: "internal",
1600
+ interrupted: true,
1601
+ interruptReason
1602
+ }
1603
+ });
1604
+ } catch (jErr) {
1605
+ logger.warn(`${route}: journal write failed after body-read interruption: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
1606
+ }
1607
+ return;
1608
+ }
1609
+ if (!res.headersSent) {
1610
+ if (existing) existing.response.status = status;
1611
+ else try {
1612
+ journal.add({
1613
+ method: req.method ?? "?",
1614
+ path: req.url ?? "?",
1615
+ headers: flattenHeaders(req.headers),
1616
+ body: null,
1617
+ ...opts?.service ? { service: opts.service } : {},
1618
+ response: {
1619
+ status,
1620
+ fixture: null,
1621
+ source: "internal"
1622
+ }
1623
+ });
1624
+ } catch (jErr) {
1625
+ logger.warn(`${route}: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
1626
+ }
1627
+ setCorsHeaders(res);
1628
+ writeErrorResponse(res, status, opts?.envelope ? opts.envelope(msg, status) : JSON.stringify({ error: {
1629
+ message: msg,
1630
+ type: errorTypeForStatus(status)
1631
+ } }));
1632
+ } else {
1633
+ const midStreamReason = clientAbort ? "client aborted" : "handler crashed mid-stream";
1634
+ if (existing) {
1635
+ if (res.writableEnded) {
1636
+ if (existing.response.error === void 0) existing.response.error = msg;
1637
+ } else if (!existing.response.interrupted) {
1638
+ existing.response.interrupted = true;
1639
+ existing.response.interruptReason = midStreamReason;
1640
+ }
1641
+ } else try {
1642
+ journal.add({
1643
+ method: req.method ?? "?",
1644
+ path: req.url ?? "?",
1645
+ headers: flattenHeaders(req.headers),
1646
+ body: null,
1647
+ ...opts?.service ? { service: opts.service } : {},
1648
+ response: {
1649
+ status: res.statusCode,
1650
+ fixture: null,
1651
+ source: "internal",
1652
+ ...res.writableEnded ? { error: msg } : {
1653
+ interrupted: true,
1654
+ interruptReason: midStreamReason
1655
+ }
1656
+ }
1657
+ });
1658
+ } catch (jErr) {
1659
+ logger.warn(`${route}: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
1660
+ }
1661
+ if (res.writableEnded) return;
1662
+ if (opts?.streamEvent) try {
1663
+ res.write(opts.streamEvent(msg, status), () => {
1664
+ if (!res.destroyed) res.destroy();
1665
+ });
1666
+ } catch (writeErr) {
1667
+ logger.debug("Failed to write error recovery response:", writeErr);
1668
+ res.destroy();
1669
+ }
1670
+ else res.destroy();
1671
+ }
1672
+ }
1673
+ for (const { handler } of mountList) {
1182
1674
  if (handler.setJournal) handler.setJournal(journal);
1183
1675
  if (registry && handler.setRegistry) handler.setRegistry(registry);
1676
+ if (handler.setLogger) handler.setLogger(logger);
1184
1677
  }
1185
1678
  if (registry) registry.setGauge("aimock_fixtures_loaded", {}, fixtures.length);
1186
1679
  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();
1680
+ requestScope.run(req, () => {
1681
+ handleHttpRequest(req, res).catch((err) => {
1682
+ routeError(req, res, err, req.url ?? "?");
1683
+ });
1200
1684
  });
1201
1685
  });
1202
1686
  async function handleHttpRequest(req, res) {
1203
1687
  const startTime = registry ? process.hrtime.bigint() : 0n;
1204
- const parsedUrl = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
1205
- let pathname = parsedUrl.pathname;
1688
+ const { id: requestId, generated: requestIdMinted } = resolveRequestId(req.headers);
1689
+ req.headers["x-request-id"] = requestId;
1690
+ markMintedRequestId(req, requestIdMinted);
1691
+ res.setHeader("X-Request-Id", requestId);
1692
+ let pathname = "";
1693
+ if (registry) {
1694
+ let recorded = false;
1695
+ const recordOutcome = (status) => {
1696
+ if (recorded) return;
1697
+ recorded = true;
1698
+ try {
1699
+ const normalizedPath = normalizePathLabel(pathname, mountList.map((m) => m.path));
1700
+ const method = req.method ?? "UNKNOWN";
1701
+ registry.incrementCounter("aimock_requests_total", {
1702
+ method,
1703
+ path: normalizedPath,
1704
+ status
1705
+ });
1706
+ const elapsed = Number(process.hrtime.bigint() - startTime) / 1e9;
1707
+ registry.observeHistogram("aimock_request_duration_seconds", {
1708
+ method,
1709
+ path: normalizedPath
1710
+ }, elapsed);
1711
+ } catch (err) {
1712
+ defaults.logger.warn("metrics instrumentation error", err);
1713
+ }
1714
+ };
1715
+ res.on("finish", () => recordOutcome(String(res.statusCode)));
1716
+ res.on("close", () => {
1717
+ if (!res.writableFinished) recordOutcome(DESTROYED_STATUS_LABEL);
1718
+ });
1719
+ }
1720
+ let parsedUrl;
1721
+ try {
1722
+ parsedUrl = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
1723
+ } catch (err) {
1724
+ const route = `${req.method ?? "?"} ${req.url ?? "?"}`;
1725
+ const msg = err instanceof Error ? err.message : "Internal error";
1726
+ logger.error(`${route}: ${msg}`);
1727
+ if (err instanceof Error && err.stack) logger.debug(err.stack);
1728
+ try {
1729
+ journal.add({
1730
+ method: req.method ?? "?",
1731
+ path: req.url ?? "?",
1732
+ headers: flattenHeaders(req.headers),
1733
+ body: null,
1734
+ response: {
1735
+ status: 400,
1736
+ fixture: null,
1737
+ source: "internal"
1738
+ }
1739
+ });
1740
+ } catch (jErr) {
1741
+ logger.warn(`${route}: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
1742
+ }
1743
+ setCorsHeaders(res);
1744
+ writeErrorResponse(res, 400, JSON.stringify({ error: {
1745
+ message: "Invalid request target",
1746
+ type: "invalid_request_error"
1747
+ } }));
1748
+ return;
1749
+ }
1750
+ pathname = parsedUrl.pathname;
1206
1751
  const originalPathname = pathname;
1207
1752
  const isOpenRouter = isOpenRouterPath(originalPathname);
1208
1753
  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
1754
  if (req.method === "OPTIONS" && (!resolvedAuth.policy.enabled || req.headers.origin !== void 0 && req.headers["access-control-request-method"] !== void 0)) {
1229
1755
  handleOptions(res);
1230
1756
  return;
1231
1757
  }
1232
- if (!(req.method === "GET" && (pathname === HEALTH_PATH || pathname === READY_PATH || pathname === "/metrics"))) {
1758
+ if (!(req.method === "GET" && (pathname === HEALTH_PATH || pathname === READY_PATH || pathname === METRICS_PATH))) {
1233
1759
  if (!validateRequestApiKey(req, resolvedAuth.policy).ok) {
1234
1760
  writeApiKeyHttpRejection(res, setCorsHeaders);
1235
1761
  return;
@@ -1237,25 +1763,18 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1237
1763
  markAuthenticatedRequest(req, resolvedAuth.policy);
1238
1764
  }
1239
1765
  if (pathname.startsWith(CONTROL_PREFIX)) {
1240
- await handleControlAPI(req, res, pathname, parsedUrl.searchParams, fixtures, journal, videoStates, openRouterVideoJobs, veoVideoJobs, grokVideoJobs, bytePlusVideoJobs, defaults);
1766
+ await handleControlAPI(req, res, pathname, parsedUrl.searchParams, fixtures, journal, videoStates, openRouterVideoJobs, veoVideoJobs, grokVideoJobs, bytePlusVideoJobs, defaults, mountList);
1241
1767
  return;
1242
1768
  }
1243
- if (mounts) {
1244
- for (const { path: mountPath, handler } of mounts) if (pathname === mountPath || pathname.startsWith(mountPath + "/")) {
1245
- const subPath = pathname.slice(mountPath.length) || "/";
1246
- if (await handler.handleRequest(req, res, subPath)) return;
1247
- }
1769
+ for (const { path: mountPath, handler } of mountList) if (pathname === mountPath || pathname.startsWith(mountPath + "/")) {
1770
+ const subPath = pathname.slice(mountPath.length) || "/";
1771
+ if (await handler.handleRequest(req, res, subPath)) return;
1248
1772
  }
1249
1773
  if (pathname === OLLAMA_CHAT_PATH && req.method === "POST") {
1250
1774
  try {
1251
1775
  await handleOllama(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
1252
1776
  } 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();
1777
+ routeError(req, res, err, pathname);
1259
1778
  }
1260
1779
  return;
1261
1780
  }
@@ -1263,12 +1782,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1263
1782
  try {
1264
1783
  await handleOllamaGenerate(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
1265
1784
  } 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();
1785
+ routeError(req, res, err, pathname);
1272
1786
  }
1273
1787
  return;
1274
1788
  }
@@ -1276,12 +1790,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1276
1790
  try {
1277
1791
  await handleOllamaEmbeddings(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
1278
1792
  } 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();
1793
+ routeError(req, res, err, pathname);
1285
1794
  }
1286
1795
  return;
1287
1796
  }
@@ -1306,28 +1815,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1306
1815
  try {
1307
1816
  await handleOpenRouterVideoContent(req, res, openRouterVideoContentMatch[1], journal, defaults, setCorsHeaders, openRouterVideoJobs);
1308
1817
  } 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();
1818
+ routeError(req, res, err, pathname);
1331
1819
  }
1332
1820
  return;
1333
1821
  }
@@ -1335,28 +1823,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1335
1823
  try {
1336
1824
  await handleOpenRouterVideoModels(req, res, fixtures, journal, defaults, setCorsHeaders);
1337
1825
  } 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();
1826
+ routeError(req, res, err, pathname);
1360
1827
  }
1361
1828
  return;
1362
1829
  }
@@ -1365,28 +1832,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1365
1832
  try {
1366
1833
  await handleOpenRouterVideoStatus(req, res, openRouterVideoStatusMatch[1], fixtures, journal, defaults, setCorsHeaders, openRouterVideoJobs);
1367
1834
  } 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();
1835
+ routeError(req, res, err, pathname);
1390
1836
  }
1391
1837
  return;
1392
1838
  }
@@ -1395,28 +1841,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1395
1841
  try {
1396
1842
  await handleOpenRouterVideoCreate(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders, openRouterVideoJobs);
1397
1843
  } 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();
1844
+ routeError(req, res, err, pathname);
1420
1845
  }
1421
1846
  return;
1422
1847
  }
@@ -1425,28 +1850,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1425
1850
  try {
1426
1851
  await handleBytePlusVideoStatus(req, res, bytePlusVideoStatusMatch[1], fixtures, journal, defaults, setCorsHeaders, bytePlusVideoJobs);
1427
1852
  } 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();
1853
+ routeError(req, res, err, pathname);
1450
1854
  }
1451
1855
  return;
1452
1856
  }
@@ -1455,40 +1859,19 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1455
1859
  try {
1456
1860
  await handleBytePlusVideoCreate(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders, bytePlusVideoJobs);
1457
1861
  } 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();
1862
+ routeError(req, res, err, pathname);
1480
1863
  }
1481
1864
  return;
1482
1865
  }
1483
- if (pathname === "/api/v1/models" && req.method === "GET") {
1866
+ if (pathname === OPENROUTER_MODELS_PATH && req.method === "GET") {
1484
1867
  handleOpenRouterModels(req, res, fixtures, journal, defaults, setCorsHeaders);
1485
1868
  return;
1486
1869
  }
1487
- if (pathname === "/api/v1/key" && req.method === "GET") {
1870
+ if (pathname === OPENROUTER_KEY_PATH && req.method === "GET") {
1488
1871
  handleOpenRouterKey(req, res, journal, setCorsHeaders);
1489
1872
  return;
1490
1873
  }
1491
- if (pathname === "/api/v1/credits" && req.method === "GET") {
1874
+ if (pathname === OPENROUTER_CREDITS_PATH && req.method === "GET") {
1492
1875
  handleOpenRouterCredits(req, res, journal, setCorsHeaders);
1493
1876
  return;
1494
1877
  }
@@ -1501,12 +1884,12 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1501
1884
  if (!azureDeploymentId) pathname = normalizeCompatPath(pathname, logger);
1502
1885
  if (pathname === HEALTH_PATH && req.method === "GET") {
1503
1886
  setCorsHeaders(res);
1504
- if (mounts && mounts.length > 0) {
1887
+ if (mountList.length > 0) {
1505
1888
  const services = { llm: {
1506
1889
  status: "ok",
1507
1890
  fixtures: fixtures.length
1508
1891
  } };
1509
- for (const { path: mountPath, handler } of mounts) if (handler.health) {
1892
+ for (const { path: mountPath, handler } of mountList) if (handler.health) {
1510
1893
  const name = mountPath.replace(/^\//, "");
1511
1894
  services[name] = handler.health();
1512
1895
  }
@@ -1527,7 +1910,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1527
1910
  res.end(JSON.stringify({ status: "ready" }));
1528
1911
  return;
1529
1912
  }
1530
- if (pathname === "/metrics" && req.method === "GET") {
1913
+ if (pathname === METRICS_PATH && req.method === "GET") {
1531
1914
  if (!registry) {
1532
1915
  handleNotFound(res, "Not found");
1533
1916
  return;
@@ -1554,14 +1937,261 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1554
1937
  }));
1555
1938
  return;
1556
1939
  }
1940
+ const batchesCancelMatch = pathname.match(BATCHES_CANCEL_RE);
1941
+ if (batchesCancelMatch && req.method === "POST") {
1942
+ try {
1943
+ await readBody(req);
1944
+ await handleBatchesCancel(req, res, batchesCancelMatch[1], journal, defaults, setCorsHeaders);
1945
+ } catch (err) {
1946
+ routeError(req, res, err, pathname, { service: "batches" });
1947
+ }
1948
+ return;
1949
+ }
1950
+ const batchesIdMatch = pathname.match(BATCHES_ID_RE);
1951
+ if (batchesIdMatch && req.method === "GET") {
1952
+ try {
1953
+ await handleBatchesRetrieve(req, res, batchesIdMatch[1], journal, defaults, setCorsHeaders);
1954
+ } catch (err) {
1955
+ routeError(req, res, err, pathname, { service: "batches" });
1956
+ }
1957
+ return;
1958
+ }
1959
+ if (pathname === BATCHES_PATH && req.method === "GET") {
1960
+ try {
1961
+ await handleBatchesList(req, res, journal, defaults, setCorsHeaders);
1962
+ } catch (err) {
1963
+ routeError(req, res, err, pathname, { service: "batches" });
1964
+ }
1965
+ return;
1966
+ }
1967
+ if (pathname === BATCHES_PATH && req.method === "POST") {
1968
+ try {
1969
+ await handleBatchesCreate(req, res, await readBody(req), journal, defaults, setCorsHeaders);
1970
+ } catch (err) {
1971
+ routeError(req, res, err, pathname, { service: "batches" });
1972
+ }
1973
+ return;
1974
+ }
1975
+ const filesContentMatch = pathname.match(FILES_CONTENT_RE);
1976
+ if (filesContentMatch && req.method === "GET") {
1977
+ try {
1978
+ await handleFilesContent(req, res, filesContentMatch[1], journal, defaults, setCorsHeaders);
1979
+ } catch (err) {
1980
+ routeError(req, res, err, pathname, { service: "files" });
1981
+ }
1982
+ return;
1983
+ }
1984
+ const filesIdMatch = pathname.match(FILES_ID_RE);
1985
+ if (filesIdMatch && req.method === "GET") {
1986
+ try {
1987
+ await handleFilesRetrieve(req, res, filesIdMatch[1], journal, defaults, setCorsHeaders);
1988
+ } catch (err) {
1989
+ routeError(req, res, err, pathname, { service: "files" });
1990
+ }
1991
+ return;
1992
+ }
1993
+ if (filesIdMatch && req.method === "DELETE") {
1994
+ try {
1995
+ await handleFilesDelete(req, res, filesIdMatch[1], journal, defaults, setCorsHeaders);
1996
+ } catch (err) {
1997
+ routeError(req, res, err, pathname, { service: "files" });
1998
+ }
1999
+ return;
2000
+ }
2001
+ if (pathname === FILES_PATH && req.method === "GET") {
2002
+ try {
2003
+ await handleFilesList(req, res, journal, defaults, setCorsHeaders);
2004
+ } catch (err) {
2005
+ routeError(req, res, err, pathname, { service: "files" });
2006
+ }
2007
+ return;
2008
+ }
2009
+ if (pathname === FILES_PATH && req.method === "POST") {
2010
+ try {
2011
+ await handleFilesCreate(req, res, (await readBodyBufferBounded(req, FILES_BODY_MAX_BYTES, FILES_BODY_DRAIN_MAX_BYTES)).buffer ?? FILES_BODY_OVERSIZED, journal, defaults, setCorsHeaders);
2012
+ } catch (err) {
2013
+ routeError(req, res, err, pathname, { service: "files" });
2014
+ }
2015
+ return;
2016
+ }
2017
+ const ftCancelMatch = pathname.match(FINE_TUNING_CANCEL_RE);
2018
+ if (ftCancelMatch && req.method === "POST") {
2019
+ try {
2020
+ await readBody(req);
2021
+ await handleFineTuningCancel(req, res, ftCancelMatch[1], journal, defaults, setCorsHeaders);
2022
+ } catch (err) {
2023
+ routeError(req, res, err, pathname, { service: "fine-tuning" });
2024
+ }
2025
+ return;
2026
+ }
2027
+ const ftEventsMatch = pathname.match(FINE_TUNING_EVENTS_RE);
2028
+ if (ftEventsMatch && req.method === "GET") {
2029
+ try {
2030
+ await handleFineTuningEvents(req, res, ftEventsMatch[1], journal, defaults, setCorsHeaders);
2031
+ } catch (err) {
2032
+ routeError(req, res, err, pathname, { service: "fine-tuning" });
2033
+ }
2034
+ return;
2035
+ }
2036
+ const ftIdMatch = pathname.match(FINE_TUNING_ID_RE);
2037
+ if (ftIdMatch && req.method === "GET") {
2038
+ try {
2039
+ await handleFineTuningRetrieve(req, res, ftIdMatch[1], journal, defaults, setCorsHeaders);
2040
+ } catch (err) {
2041
+ routeError(req, res, err, pathname, { service: "fine-tuning" });
2042
+ }
2043
+ return;
2044
+ }
2045
+ if (pathname === FINE_TUNING_JOBS_PATH && req.method === "GET") {
2046
+ try {
2047
+ await handleFineTuningList(req, res, journal, defaults, setCorsHeaders);
2048
+ } catch (err) {
2049
+ routeError(req, res, err, pathname, { service: "fine-tuning" });
2050
+ }
2051
+ return;
2052
+ }
2053
+ if (pathname === FINE_TUNING_JOBS_PATH && req.method === "POST") {
2054
+ try {
2055
+ await handleFineTuningCreate(req, res, await readBody(req), journal, defaults, setCorsHeaders);
2056
+ } catch (err) {
2057
+ routeError(req, res, err, pathname, { service: "fine-tuning" });
2058
+ }
2059
+ return;
2060
+ }
2061
+ const vsSearchMatch = pathname.match(VECTOR_STORES_SEARCH_RE);
2062
+ if (vsSearchMatch && req.method === "POST") {
2063
+ try {
2064
+ const raw = await readBody(req);
2065
+ await handleVectorStoresSearch(req, res, vsSearchMatch[1], raw, journal, defaults, setCorsHeaders);
2066
+ } catch (err) {
2067
+ routeError(req, res, err, pathname, { service: "vector-stores" });
2068
+ }
2069
+ return;
2070
+ }
2071
+ const vsBatchFilesMatch = pathname.match(VECTOR_STORES_BATCH_FILES_RE);
2072
+ if (vsBatchFilesMatch && req.method === "GET") {
2073
+ try {
2074
+ await handleVectorFileBatchesFiles(req, res, vsBatchFilesMatch[1], vsBatchFilesMatch[2], journal, defaults, setCorsHeaders);
2075
+ } catch (err) {
2076
+ routeError(req, res, err, pathname, { service: "vector-stores" });
2077
+ }
2078
+ return;
2079
+ }
2080
+ const vsBatchCancelMatch = pathname.match(VECTOR_STORES_BATCH_CANCEL_RE);
2081
+ if (vsBatchCancelMatch && req.method === "POST") {
2082
+ try {
2083
+ await readBody(req);
2084
+ await handleVectorFileBatchesCancel(req, res, vsBatchCancelMatch[1], vsBatchCancelMatch[2], journal, defaults, setCorsHeaders);
2085
+ } catch (err) {
2086
+ routeError(req, res, err, pathname, { service: "vector-stores" });
2087
+ }
2088
+ return;
2089
+ }
2090
+ const vsBatchMatch = pathname.match(VECTOR_STORES_BATCH_RE);
2091
+ if (vsBatchMatch && req.method === "GET") {
2092
+ try {
2093
+ await handleVectorFileBatchesRetrieve(req, res, vsBatchMatch[1], vsBatchMatch[2], journal, defaults, setCorsHeaders);
2094
+ } catch (err) {
2095
+ routeError(req, res, err, pathname, { service: "vector-stores" });
2096
+ }
2097
+ return;
2098
+ }
2099
+ const vsFileMatch = pathname.match(VECTOR_STORES_FILE_RE);
2100
+ if (vsFileMatch && req.method === "GET") {
2101
+ try {
2102
+ await handleVectorStoreFilesRetrieve(req, res, vsFileMatch[1], vsFileMatch[2], journal, defaults, setCorsHeaders);
2103
+ } catch (err) {
2104
+ routeError(req, res, err, pathname, { service: "vector-stores" });
2105
+ }
2106
+ return;
2107
+ }
2108
+ if (vsFileMatch && req.method === "DELETE") {
2109
+ try {
2110
+ await handleVectorStoreFilesDelete(req, res, vsFileMatch[1], vsFileMatch[2], journal, defaults, setCorsHeaders);
2111
+ } catch (err) {
2112
+ routeError(req, res, err, pathname, { service: "vector-stores" });
2113
+ }
2114
+ return;
2115
+ }
2116
+ const vsIdMatch = pathname.match(VECTOR_STORES_ID_RE);
2117
+ if (vsIdMatch && req.method === "GET") {
2118
+ try {
2119
+ await handleVectorStoresRetrieve(req, res, vsIdMatch[1], journal, defaults, setCorsHeaders);
2120
+ } catch (err) {
2121
+ routeError(req, res, err, pathname, { service: "vector-stores" });
2122
+ }
2123
+ return;
2124
+ }
2125
+ if (vsIdMatch && req.method === "POST") {
2126
+ try {
2127
+ const raw = await readBody(req);
2128
+ await handleVectorStoresModify(req, res, vsIdMatch[1], raw, journal, defaults, setCorsHeaders);
2129
+ } catch (err) {
2130
+ routeError(req, res, err, pathname, { service: "vector-stores" });
2131
+ }
2132
+ return;
2133
+ }
2134
+ if (vsIdMatch && req.method === "DELETE") {
2135
+ try {
2136
+ await handleVectorStoresDelete(req, res, vsIdMatch[1], journal, defaults, setCorsHeaders);
2137
+ } catch (err) {
2138
+ routeError(req, res, err, pathname, { service: "vector-stores" });
2139
+ }
2140
+ return;
2141
+ }
2142
+ if (VECTOR_STORES_FILES_RE.test(pathname) && req.method === "POST") {
2143
+ try {
2144
+ const raw = await readBody(req);
2145
+ const storeId = pathname.split("/")[3];
2146
+ await handleVectorStoreFilesCreate(req, res, storeId, raw, journal, defaults, setCorsHeaders);
2147
+ } catch (err) {
2148
+ routeError(req, res, err, pathname, { service: "vector-stores" });
2149
+ }
2150
+ return;
2151
+ }
2152
+ if (VECTOR_STORES_FILES_RE.test(pathname) && req.method === "GET") {
2153
+ try {
2154
+ const storeId = pathname.split("/")[3];
2155
+ await handleVectorStoreFilesList(req, res, storeId, journal, defaults, setCorsHeaders);
2156
+ } catch (err) {
2157
+ routeError(req, res, err, pathname, { service: "vector-stores" });
2158
+ }
2159
+ return;
2160
+ }
2161
+ if (VECTOR_STORES_FILE_BATCHES_RE.test(pathname) && req.method === "POST") {
2162
+ try {
2163
+ const raw = await readBody(req);
2164
+ const storeId = pathname.split("/")[3];
2165
+ await handleVectorFileBatchesCreate(req, res, storeId, raw, journal, defaults, setCorsHeaders);
2166
+ } catch (err) {
2167
+ routeError(req, res, err, pathname, { service: "vector-stores" });
2168
+ }
2169
+ return;
2170
+ }
2171
+ if (pathname === VECTOR_STORES_PATH && req.method === "GET") {
2172
+ try {
2173
+ await handleVectorStoresList(req, res, journal, defaults, setCorsHeaders);
2174
+ } catch (err) {
2175
+ routeError(req, res, err, pathname, { service: "vector-stores" });
2176
+ }
2177
+ return;
2178
+ }
2179
+ if (pathname === VECTOR_STORES_PATH && req.method === "POST") {
2180
+ try {
2181
+ await handleVectorStoresCreate(req, res, await readBody(req), journal, defaults, setCorsHeaders);
2182
+ } catch (err) {
2183
+ routeError(req, res, err, pathname, { service: "vector-stores" });
2184
+ }
2185
+ return;
2186
+ }
1557
2187
  if (pathname === REQUESTS_PATH) {
1558
2188
  setCorsHeaders(res);
1559
2189
  if (req.method === "GET") {
1560
2190
  const limitParam = parsedUrl.searchParams.get("limit");
1561
2191
  let opts;
1562
2192
  if (limitParam) {
1563
- const limit = parseInt(limitParam, 10);
1564
- if (Number.isNaN(limit) || limit <= 0) {
2193
+ const limit = Number(limitParam);
2194
+ if (!/^\+?\d+$/.test(limitParam.trim()) || !Number.isSafeInteger(limit) || limit <= 0) {
1565
2195
  writeErrorResponse(res, 400, JSON.stringify({ error: {
1566
2196
  message: `Invalid limit parameter: "${limitParam}"`,
1567
2197
  type: "invalid_request_error"
@@ -1588,17 +2218,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1588
2218
  try {
1589
2219
  await handleResponses(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
1590
2220
  } 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
- }
2221
+ routeError(req, res, err, pathname, { streamEvent: (m) => `event: error\ndata: ${JSON.stringify({ error: { message: m } })}\n\n` });
1602
2222
  }
1603
2223
  return;
1604
2224
  }
@@ -1606,17 +2226,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1606
2226
  try {
1607
2227
  await handleMessages(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
1608
2228
  } 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
- }
2229
+ routeError(req, res, err, pathname, { streamEvent: (m) => `event: error\ndata: ${JSON.stringify({ error: { message: m } })}\n\n` });
1620
2230
  }
1621
2231
  return;
1622
2232
  }
@@ -1624,17 +2234,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1624
2234
  try {
1625
2235
  await handleCohere(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
1626
2236
  } 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
- }
2237
+ routeError(req, res, err, pathname, { streamEvent: (m) => `event: error\ndata: ${JSON.stringify({ error: { message: m } })}\n\n` });
1638
2238
  }
1639
2239
  return;
1640
2240
  }
@@ -1642,12 +2242,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1642
2242
  try {
1643
2243
  await handleCohereEmbed(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
1644
2244
  } 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();
2245
+ routeError(req, res, err, pathname);
1651
2246
  }
1652
2247
  return;
1653
2248
  }
@@ -1665,14 +2260,9 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1665
2260
  } catch (err) {
1666
2261
  if (!(err instanceof SyntaxError)) defaults.logger.error(`Unexpected error in Azure model injection: ${err instanceof Error ? err.message : String(err)}`);
1667
2262
  }
1668
- await handleEmbeddings(req, res, raw, fixtures, journal, defaults, setCorsHeaders, embeddingsProvider);
2263
+ await handleEmbeddings(req, res, raw, fixtures, journal, defaults, setCorsHeaders, embeddingsProvider, originalPathname === EMBEDDINGS_PATH);
1669
2264
  } 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();
2265
+ routeError(req, res, err, pathname);
1676
2266
  }
1677
2267
  return;
1678
2268
  }
@@ -1680,12 +2270,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1680
2270
  try {
1681
2271
  await handleImages(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders, "openai", void 0, isBytePlusArk ? "byteplus" : void 0);
1682
2272
  } 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();
2273
+ routeError(req, res, err, pathname);
1689
2274
  }
1690
2275
  return;
1691
2276
  }
@@ -1693,12 +2278,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1693
2278
  try {
1694
2279
  await handleImageEdit(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
1695
2280
  } 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();
2281
+ routeError(req, res, err, pathname);
1702
2282
  }
1703
2283
  return;
1704
2284
  }
@@ -1706,12 +2286,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1706
2286
  try {
1707
2287
  await handleImageVariations(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
1708
2288
  } 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();
2289
+ routeError(req, res, err, pathname);
1715
2290
  }
1716
2291
  return;
1717
2292
  }
@@ -1719,12 +2294,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1719
2294
  try {
1720
2295
  await handleSpeech(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
1721
2296
  } 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();
2297
+ routeError(req, res, err, pathname);
1728
2298
  }
1729
2299
  return;
1730
2300
  }
@@ -1732,12 +2302,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1732
2302
  try {
1733
2303
  await handleTranscription(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
1734
2304
  } 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();
2305
+ routeError(req, res, err, pathname);
1741
2306
  }
1742
2307
  return;
1743
2308
  }
@@ -1745,12 +2310,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1745
2310
  try {
1746
2311
  await handleTranscription(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders, "translation");
1747
2312
  } 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();
2313
+ routeError(req, res, err, pathname);
1754
2314
  }
1755
2315
  return;
1756
2316
  }
@@ -1759,28 +2319,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1759
2319
  try {
1760
2320
  await handleGrokVideoCreate(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders, grokVideoJobs);
1761
2321
  } 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();
2322
+ routeError(req, res, err, pathname);
1784
2323
  }
1785
2324
  return;
1786
2325
  }
@@ -1788,12 +2327,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1788
2327
  try {
1789
2328
  await handleVideoCreate(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders, videoStates);
1790
2329
  } 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();
2330
+ routeError(req, res, err, pathname);
1797
2331
  }
1798
2332
  return;
1799
2333
  }
@@ -1802,28 +2336,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1802
2336
  try {
1803
2337
  await handleGrokVideoStatus(req, res, grokVideoStatusMatch[1], fixtures, journal, defaults, setCorsHeaders, grokVideoJobs, videoStates);
1804
2338
  } 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();
2339
+ routeError(req, res, err, pathname);
1827
2340
  }
1828
2341
  return;
1829
2342
  }
@@ -1833,28 +2346,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1833
2346
  try {
1834
2347
  await handleVeoVideoCreate(req, res, await readBody(req), veoPredictMatch[1], fixtures, journal, defaults, setCorsHeaders, veoVideoJobs);
1835
2348
  } 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();
2349
+ routeError(req, res, err, pathname);
1858
2350
  }
1859
2351
  return;
1860
2352
  }
@@ -1863,28 +2355,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1863
2355
  try {
1864
2356
  await handleVeoVideoStatus(req, res, veoOperationMatch[1], fixtures, journal, defaults, setCorsHeaders, veoVideoJobs);
1865
2357
  } 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();
2358
+ routeError(req, res, err, pathname);
1888
2359
  }
1889
2360
  return;
1890
2361
  }
@@ -1894,12 +2365,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1894
2365
  try {
1895
2366
  await handleImages(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders, "gemini", predictModel);
1896
2367
  } 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();
2368
+ routeError(req, res, err, pathname);
1903
2369
  }
1904
2370
  return;
1905
2371
  }
@@ -1907,17 +2373,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1907
2373
  try {
1908
2374
  await handleGeminiInteractions(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
1909
2375
  } 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
- }
2376
+ routeError(req, res, err, pathname, { streamEvent: (m) => `data: ${JSON.stringify({ error: { message: m } })}\n\n` });
1921
2377
  }
1922
2378
  return;
1923
2379
  }
@@ -1927,12 +2383,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1927
2383
  try {
1928
2384
  await handleGeminiEmbedContent(req, res, await readBody(req), embedModel, fixtures, journal, defaults, setCorsHeaders);
1929
2385
  } 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();
2386
+ routeError(req, res, err, pathname);
1936
2387
  }
1937
2388
  return;
1938
2389
  }
@@ -1943,17 +2394,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1943
2394
  try {
1944
2395
  await handleGemini(req, res, await readBody(req), geminiModel, streaming, fixtures, journal, defaults, setCorsHeaders);
1945
2396
  } 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
- }
2397
+ routeError(req, res, err, pathname, { streamEvent: (m) => `data: ${JSON.stringify({ error: { message: m } })}\n\n` });
1957
2398
  }
1958
2399
  return;
1959
2400
  }
@@ -1964,17 +2405,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1964
2405
  try {
1965
2406
  await handleGemini(req, res, await readBody(req), vertexModel, streaming, fixtures, journal, defaults, setCorsHeaders, "vertexai");
1966
2407
  } 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
- }
2408
+ routeError(req, res, err, pathname, { streamEvent: (m) => `data: ${JSON.stringify({ error: { message: m } })}\n\n` });
1978
2409
  }
1979
2410
  return;
1980
2411
  }
@@ -1984,12 +2415,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1984
2415
  try {
1985
2416
  await handleBedrock(req, res, await readBody(req), bedrockModelId, fixtures, journal, defaults, setCorsHeaders);
1986
2417
  } 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();
2418
+ routeError(req, res, err, pathname);
1993
2419
  }
1994
2420
  return;
1995
2421
  }
@@ -1999,12 +2425,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1999
2425
  try {
2000
2426
  await handleBedrockStream(req, res, await readBody(req), bedrockModelId, fixtures, journal, defaults, setCorsHeaders);
2001
2427
  } 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();
2428
+ routeError(req, res, err, pathname);
2008
2429
  }
2009
2430
  return;
2010
2431
  }
@@ -2014,12 +2435,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2014
2435
  try {
2015
2436
  await handleConverse(req, res, await readBody(req), converseModelId, fixtures, journal, defaults, setCorsHeaders);
2016
2437
  } 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();
2438
+ routeError(req, res, err, pathname);
2023
2439
  }
2024
2440
  return;
2025
2441
  }
@@ -2029,12 +2445,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2029
2445
  try {
2030
2446
  await handleConverseStream(req, res, await readBody(req), converseStreamModelId, fixtures, journal, defaults, setCorsHeaders);
2031
2447
  } 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();
2448
+ routeError(req, res, err, pathname);
2038
2449
  }
2039
2450
  return;
2040
2451
  }
@@ -2042,12 +2453,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2042
2453
  try {
2043
2454
  await handleSearch(req, res, await readBody(req), serviceFixtures?.search ?? [], journal, defaults, setCorsHeaders);
2044
2455
  } 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();
2456
+ routeError(req, res, err, pathname, { service: "search" });
2051
2457
  }
2052
2458
  return;
2053
2459
  }
@@ -2055,12 +2461,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2055
2461
  try {
2056
2462
  await handleRerank(req, res, await readBody(req), serviceFixtures?.rerank ?? [], journal, defaults, setCorsHeaders);
2057
2463
  } 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();
2464
+ routeError(req, res, err, pathname, { service: "rerank" });
2064
2465
  }
2065
2466
  return;
2066
2467
  }
@@ -2068,12 +2469,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2068
2469
  try {
2069
2470
  await handleModeration(req, res, await readBody(req), serviceFixtures?.moderation ?? [], journal, defaults, setCorsHeaders);
2070
2471
  } 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();
2472
+ routeError(req, res, err, pathname, { service: "moderation" });
2077
2473
  }
2078
2474
  return;
2079
2475
  }
@@ -2082,12 +2478,57 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2082
2478
  try {
2083
2479
  await handleElevenLabsAudio(req, res, await readBody(req), fixtures, defaults, journal, "sound-generation");
2084
2480
  } 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"
2481
+ routeError(req, res, err, pathname);
2482
+ }
2483
+ return;
2484
+ }
2485
+ if (pathname === ELEVENLABS_VOICE_DESIGN_PATH && req.method === "POST") {
2486
+ setCorsHeaders(res);
2487
+ try {
2488
+ await handleElevenLabsVoiceDesign(req, res, await readBody(req), fixtures, defaults, journal);
2489
+ } catch (err) {
2490
+ routeError(req, res, err, pathname, { service: "elevenlabs-voice" });
2491
+ }
2492
+ return;
2493
+ }
2494
+ if (pathname === ELEVENLABS_VOICE_CREATE_PATH && req.method === "POST") {
2495
+ setCorsHeaders(res);
2496
+ try {
2497
+ await handleElevenLabsVoiceCreate(req, res, await readBody(req), fixtures, defaults, journal);
2498
+ } catch (err) {
2499
+ routeError(req, res, err, pathname, { service: "elevenlabs-voice" });
2500
+ }
2501
+ return;
2502
+ }
2503
+ const elevenLabsVoiceMatch = pathname.match(ELEVENLABS_VOICE_RE);
2504
+ if (elevenLabsVoiceMatch && (req.method === "GET" || req.method === "DELETE")) {
2505
+ setCorsHeaders(res);
2506
+ let voiceId;
2507
+ try {
2508
+ voiceId = decodeURIComponent(elevenLabsVoiceMatch[1]);
2509
+ } catch {
2510
+ writeErrorResponse(res, 400, JSON.stringify({ error: {
2511
+ message: `Invalid voice id '${elevenLabsVoiceMatch[1]}': malformed percent-encoding`,
2512
+ type: "invalid_request_error"
2089
2513
  } }));
2090
- else if (!res.writableEnded) res.destroy();
2514
+ journal.add({
2515
+ method: req.method ?? "GET",
2516
+ path: req.url ?? pathname,
2517
+ headers: flattenHeaders(req.headers),
2518
+ service: "elevenlabs-voice",
2519
+ body: null,
2520
+ response: {
2521
+ status: 400,
2522
+ fixture: null
2523
+ }
2524
+ });
2525
+ return;
2526
+ }
2527
+ try {
2528
+ if (req.method === "GET") await handleElevenLabsVoiceGet(req, res, voiceId, fixtures, defaults, journal);
2529
+ else await handleElevenLabsVoiceDelete(req, res, voiceId, fixtures, defaults, journal);
2530
+ } catch (err) {
2531
+ routeError(req, res, err, pathname, { service: "elevenlabs-voice" });
2091
2532
  }
2092
2533
  return;
2093
2534
  }
@@ -2098,12 +2539,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2098
2539
  try {
2099
2540
  await handleElevenLabsTTS(req, res, await readBody(req), fixtures, defaults, journal, voiceId);
2100
2541
  } 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();
2542
+ routeError(req, res, err, pathname);
2107
2543
  }
2108
2544
  return;
2109
2545
  }
@@ -2114,42 +2550,38 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2114
2550
  try {
2115
2551
  await handleElevenLabsAudio(req, res, await readBody(req), fixtures, defaults, journal, musicSubType);
2116
2552
  } 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();
2553
+ routeError(req, res, err, pathname);
2123
2554
  }
2124
2555
  return;
2125
2556
  }
2126
2557
  let falBody;
2127
- if (FAL_PREFIX_RE.test(pathname) && req.headers["x-fal-target-host"]) {
2558
+ if (FAL_ROUTE_RE.test(pathname) && req.headers["x-fal-target-host"]) {
2128
2559
  setCorsHeaders(res);
2129
2560
  try {
2130
2561
  falBody = req.method === "POST" || req.method === "PUT" ? await readBody(req) : "";
2131
2562
  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;
2563
+ if (falWillHandle(req, pathname)) {
2564
+ const chaosConfig = resolveChaosConfig(null, defaults.chaos, req.headers, defaults.logger, req.url);
2565
+ await awaitChaosLatency(null, defaults.chaos, req.headers, defaults.logger, req.url, res, chaosConfig);
2566
+ const goneAfterLatency = responseGoneReason(res);
2567
+ if (goneAfterLatency !== null) {
2568
+ defaults.logger.debug(`[chaos] ${req.method ?? "GET"} ${pathname}: ${describeUnwritableReason(goneAfterLatency)} after the latency delay — not served, not journalled`);
2569
+ return;
2570
+ }
2571
+ const chaosAction = evaluateChaos(null, defaults.chaos, req.headers, defaults.logger, req.url, chaosConfig);
2572
+ if (chaosAction) {
2573
+ applyChaosAction(chaosAction, res, null, journal, {
2574
+ method: req.method ?? "GET",
2575
+ path: pathname,
2576
+ headers: flattenHeaders(req.headers),
2577
+ body: null
2578
+ }, "internal", defaults.registry, defaults.logger);
2579
+ return;
2580
+ }
2144
2581
  }
2145
2582
  if (await handleFal(req, res, raw, pathname, fixtures, defaults, journal) === "handled") return;
2146
2583
  } 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();
2584
+ routeError(req, res, err, pathname);
2153
2585
  return;
2154
2586
  }
2155
2587
  }
@@ -2158,12 +2590,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2158
2590
  try {
2159
2591
  await handleFalQueue(req, res, falBody ?? await readBody(req), pathname, fixtures, defaults, journal);
2160
2592
  } 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();
2593
+ routeError(req, res, err, pathname);
2167
2594
  }
2168
2595
  return;
2169
2596
  }
@@ -2171,27 +2598,26 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2171
2598
  setCorsHeaders(res);
2172
2599
  try {
2173
2600
  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);
2601
+ const chaosConfig = resolveChaosConfig(null, defaults.chaos, req.headers, defaults.logger, req.url);
2602
+ await awaitChaosLatency(null, defaults.chaos, req.headers, defaults.logger, req.url, res, chaosConfig);
2603
+ const goneAfterLatency = responseGoneReason(res);
2604
+ if (goneAfterLatency !== null) {
2605
+ defaults.logger.debug(`[chaos] ${req.method ?? "GET"} ${pathname}: ${describeUnwritableReason(goneAfterLatency)} after the latency delay — not served, not journalled`);
2606
+ return;
2607
+ }
2608
+ const chaosAction = evaluateChaos(null, defaults.chaos, req.headers, defaults.logger, req.url, chaosConfig);
2175
2609
  if (chaosAction) {
2176
2610
  applyChaosAction(chaosAction, res, null, journal, {
2177
2611
  method: req.method ?? "GET",
2178
2612
  path: pathname,
2179
2613
  headers: flattenHeaders(req.headers),
2180
- body: {
2181
- model: "",
2182
- messages: []
2183
- }
2184
- }, "fixture", defaults.registry);
2614
+ body: null
2615
+ }, "internal", defaults.registry, defaults.logger);
2185
2616
  return;
2186
2617
  }
2187
2618
  await handleFalQueue(req, res, raw, pathname, fixtures, defaults, journal);
2188
2619
  } 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();
2620
+ routeError(req, res, err, pathname);
2195
2621
  }
2196
2622
  return;
2197
2623
  }
@@ -2200,16 +2626,13 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2200
2626
  try {
2201
2627
  await handleFalQueue(req, res, falBody ?? await readBody(req), pathname, fixtures, defaults, journal);
2202
2628
  } 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();
2629
+ routeError(req, res, err, pathname);
2209
2630
  }
2210
2631
  return;
2211
2632
  }
2212
2633
  if (pathname !== COMPLETIONS_PATH) {
2634
+ const catalogMatch = matchRouteDefinition(req.method ?? "", pathname);
2635
+ if (catalogMatch && !(pathname === METRICS_PATH && !registry)) defaults.logger.warn(`Route drift: ${req.method ?? "?"} ${pathname} matches the route catalog (${catalogMatch.method} ${catalogMatch.path} — ${catalogMatch.description}) but no dispatcher branch served it. The catalog entry or the dispatcher is stale — see route-registry.ts.`);
2213
2636
  handleNotFound(res, "Not found");
2214
2637
  return;
2215
2638
  }
@@ -2221,23 +2644,25 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2221
2644
  try {
2222
2645
  await handleCompletions(req, res, fixtures, journal, defaults, azureDeploymentId, completionsProvider, isOpenRouter);
2223
2646
  } 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
- }
2647
+ routeError(req, res, err, pathname, {
2648
+ envelope: (m, status) => isOpenRouter ? serializeOpenRouterError(status, m) : JSON.stringify({ error: {
2649
+ message: m,
2650
+ type: errorTypeForStatus(status)
2651
+ } }),
2652
+ streamEvent: (m, status) => `data: ${isOpenRouter ? serializeOpenRouterError(status, m) : JSON.stringify({ error: {
2653
+ message: m,
2654
+ type: errorTypeForStatus(status)
2655
+ } })}\n\n`
2656
+ });
2238
2657
  }
2239
2658
  }
2240
2659
  const activeConnections = /* @__PURE__ */ new Set();
2660
+ const liveLimits = normalizeLiveOptions(options?.live);
2661
+ const liveSessions = /* @__PURE__ */ new Map();
2662
+ const closeLiveSessions = (testId) => {
2663
+ for (const session of [...liveSessions.values()]) if (testId === void 0 || session.testId === testId) session.dispose();
2664
+ };
2665
+ liveClosers.set(defaults, closeLiveSessions);
2241
2666
  server.on("upgrade", (req, socket, head) => {
2242
2667
  handleUpgradeRequest(req, socket, head).catch((err) => {
2243
2668
  const msg = err instanceof Error ? err.message : "Internal error";
@@ -2287,30 +2712,57 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2287
2712
  server.emit("connection", socket);
2288
2713
  return;
2289
2714
  }
2290
- const parsedUrl = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
2715
+ let parsedUrl;
2716
+ try {
2717
+ parsedUrl = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
2718
+ } catch (err) {
2719
+ const msg = err instanceof Error ? err.message : "Internal error";
2720
+ defaults.logger.warn(`Unhandled upgrade error: ${msg}`);
2721
+ socket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
2722
+ return;
2723
+ }
2291
2724
  let pathname = parsedUrl.pathname;
2292
2725
  if (!validateRequestApiKey(req, resolvedAuth.policy).ok) {
2293
2726
  writeApiKeyUpgradeRejection(socket);
2294
2727
  return;
2295
2728
  }
2296
- if (mounts) {
2297
- for (const { path: mountPath, handler } of mounts) if ((pathname === mountPath || pathname.startsWith(mountPath + "/")) && handler.handleUpgrade) {
2298
- const subPath = pathname.slice(mountPath.length) || "/";
2299
- if (await handler.handleUpgrade(socket, head, subPath)) return;
2300
- }
2729
+ for (const { path: mountPath, handler } of mountList) if ((pathname === mountPath || pathname.startsWith(mountPath + "/")) && handler.handleUpgrade) {
2730
+ const subPath = pathname.slice(mountPath.length) || "/";
2731
+ if (await handler.handleUpgrade(socket, head, subPath)) return;
2301
2732
  }
2302
2733
  if (!pathname.match(AZURE_DEPLOYMENT_RE)) pathname = normalizeCompatPath(pathname, logger);
2303
- if (pathname !== RESPONSES_PATH && pathname !== REALTIME_PATH && pathname !== GEMINI_LIVE_PATH) {
2734
+ if (pathname !== RESPONSES_PATH && pathname !== REALTIME_PATH && pathname !== GEMINI_LIVE_PATH && pathname !== LIVE_PATH) {
2304
2735
  socket.write("HTTP/1.1 404 Not Found\r\n\r\n");
2305
2736
  socket.destroy();
2306
2737
  return;
2307
2738
  }
2739
+ let liveId;
2740
+ if (pathname === LIVE_PATH) {
2741
+ if (req.method !== "GET" || parsedUrl.searchParams.has("model")) {
2742
+ socket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
2743
+ return;
2744
+ }
2745
+ if (liveSessions.size >= liveLimits.maxSessions) {
2746
+ socket.end("HTTP/1.1 429 Too Many Requests\r\nConnection: close\r\n\r\n");
2747
+ return;
2748
+ }
2749
+ liveId = Symbol("live-session");
2750
+ const reservedId = liveId;
2751
+ liveSessions.set(reservedId, {
2752
+ testId: getTestId(req),
2753
+ dispose: () => {
2754
+ socket.destroy();
2755
+ liveSessions.delete(reservedId);
2756
+ }
2757
+ });
2758
+ }
2308
2759
  if (head.length > 0) socket.unshift(head);
2309
2760
  let ws;
2310
2761
  try {
2311
- ws = upgradeToWebSocket(req, socket);
2762
+ ws = upgradeToWebSocket(req, socket, pathname === LIVE_PATH ? liveLimits : void 0);
2312
2763
  } catch (err) {
2313
2764
  const msg = err instanceof Error ? err.message : "WebSocket upgrade failed";
2765
+ if (liveId !== void 0) liveSessions.delete(liveId);
2314
2766
  logger.error(`WebSocket upgrade error: ${msg}`);
2315
2767
  if (!socket.destroyed) socket.destroy();
2316
2768
  return;
@@ -2324,7 +2776,24 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2324
2776
  activeConnections.delete(ws);
2325
2777
  });
2326
2778
  const wsTestId = getTestId(req);
2327
- if (pathname === RESPONSES_PATH) handleWebSocketResponses(ws, fixtures, journal, {
2779
+ if (pathname === LIVE_PATH && liveId !== void 0) {
2780
+ const sessionId = liveId;
2781
+ const dispose = handleLiveSession(ws, fixtures, journal, {
2782
+ defaults,
2783
+ limits: liveLimits,
2784
+ localApiKeys: resolvedAuth.publicConfig?.apiKeys,
2785
+ testId: wsTestId,
2786
+ headers: req.headers,
2787
+ onDispose: () => {
2788
+ liveSessions.delete(sessionId);
2789
+ activeConnections.delete(ws);
2790
+ }
2791
+ });
2792
+ liveSessions.set(sessionId, {
2793
+ testId: wsTestId,
2794
+ dispose
2795
+ });
2796
+ } else if (pathname === RESPONSES_PATH) handleWebSocketResponses(ws, fixtures, journal, {
2328
2797
  ...defaults,
2329
2798
  model: "gpt-4",
2330
2799
  testId: wsTestId,
@@ -2349,6 +2818,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2349
2818
  }
2350
2819
  const originalClose = server.close.bind(server);
2351
2820
  server.close = function(callback) {
2821
+ closeLiveSessions();
2352
2822
  for (const ws of activeConnections) ws.close(1001, "Server shutting down");
2353
2823
  activeConnections.clear();
2354
2824
  videoStates.clear();
@@ -2368,10 +2838,15 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
2368
2838
  return;
2369
2839
  }
2370
2840
  const url = `http://${addr.address}:${addr.port}`;
2371
- if (mounts) {
2372
- for (const { path: mountPath, handler } of mounts) if (handler.setBaseUrl) handler.setBaseUrl(url + mountPath);
2841
+ try {
2842
+ commitHandOff();
2843
+ } catch (err) {
2844
+ server.close(() => reject(err));
2845
+ return;
2373
2846
  }
2847
+ for (const { path: mountPath, handler } of mountList) if (handler.setBaseUrl) handler.setBaseUrl(url + mountPath);
2374
2848
  resolve({
2849
+ closeLiveSessions,
2375
2850
  server,
2376
2851
  journal,
2377
2852
  url,