@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/files.cjs ADDED
@@ -0,0 +1,1069 @@
1
+ const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
2
+ const require_helpers = require('./helpers.cjs');
3
+ const require_chaos = require('./chaos.cjs');
4
+ const require_vector_stores = require('./vector-stores.cjs');
5
+ let node_crypto = require("node:crypto");
6
+
7
+ //#region src/files.ts
8
+ /**
9
+ * OpenAI Files API mock for aimock.
10
+ *
11
+ * Implements the subset of `POST /v1/files`, `GET /v1/files`,
12
+ * `GET /v1/files/{id}`, `GET /v1/files/{id}/content` and
13
+ * `DELETE /v1/files/{id}` that test harnesses need to exercise
14
+ * file-upload flows without touching the real API.
15
+ *
16
+ * Storage is in-memory and per-process (like the video job maps):
17
+ * - metadata lives in `fileStore`
18
+ * - raw bytes live in `fileContents` as `Buffer`s. Bytes are never
19
+ * round-tripped through a utf8 string, so binary uploads come back
20
+ * byte-for-byte identical.
21
+ * - `clearFileStore()` wipes both and is wired into the full reset path
22
+ * (`POST /__aimock/reset` + `LLMock.reset()`)
23
+ *
24
+ * The only memory bound here is per file: {@link FILES_MAX_BYTES} caps one
25
+ * upload. The file COUNT is deliberately unbounded, so total residency grows
26
+ * with the number of uploads until something calls `clearFileStore()` (or the
27
+ * process exits) — a suite that uploads in a loop without resetting will grow
28
+ * this map without limit. That is a real, documented bound, not an OOM defense.
29
+ *
30
+ * The job maps this module otherwise mirrors (fal-audio.ts,
31
+ * openrouter-video.ts, grok-video.ts, byteplus-video.ts) DO cap entry count and
32
+ * FIFO-evict the oldest, and that convention deliberately does NOT apply here.
33
+ * Those hold transient jobs that the real API also expires, so eviction
34
+ * reproduces real behaviour. A file is a durable object: the real API keeps it
35
+ * until the caller deletes it. Silently evicting one would make the mock answer
36
+ * `404 No such file` for a file the caller successfully uploaded and never
37
+ * deleted — an outcome the real API cannot produce, surfacing as an
38
+ * unexplainable flake in the suite under test. Unbounded growth is the honest
39
+ * failure mode; `POST /__aimock/reset` is the lever that reclaims it.
40
+ *
41
+ * Body handling accepts both shapes real SDKs send:
42
+ * - JSON `{ filename, purpose, content }` (test-friendly; `bytes` is an alias
43
+ * of `content`, and exactly one of the two must be present)
44
+ * - `multipart/form-data` with `purpose` + exactly one payload part named
45
+ * `file` (or its `content` alias), framed per RFC 2046 §5.1.1 so a payload
46
+ * containing the boundary token is stored whole rather than truncated at it
47
+ *
48
+ * Both parsers are strict: a malformed body is an OpenAI-shaped
49
+ * `400 invalid_request_error` naming the offending field or part. Nothing is
50
+ * `String()`-coerced and no missing field falls through to a fabricated
51
+ * default, because a mock that quietly stores `"[object Object]"` or a 0-byte
52
+ * file teaches the suite under test that a broken upload succeeded.
53
+ *
54
+ * Every branch journals with `service: "files"` so
55
+ * `GET /__aimock/journal?service=files` selects exactly this traffic, and with
56
+ * `source: "internal"` because — as with fine-tuning — no fixture or proxy
57
+ * ever serves this store. A successful upload journals a SYNTHETIC body
58
+ * describing it (`{ purpose, filename, bytes, content_type, sha256 }`) — never
59
+ * the uploaded octets — exactly as the transcription route journals its
60
+ * multipart audio; a `400` journals the error envelope it answered with, so
61
+ * the reason is readable from the journal; see {@link journalFiles}. Files also
62
+ * honors the inbound API-key boundary via the server dispatch (no bypass),
63
+ * and runs through the chaos gate so retry/backoff suites can inject
64
+ * 500s at the files surface too.
65
+ */
66
+ /**
67
+ * Purposes accepted on upload — `CreateFileRequest.purpose` in the official
68
+ * `openai/openai-openapi` spec (and the `FilePurpose` type in the SDK).
69
+ * `user_data` is what Responses-API file inputs use, so rejecting it broke the
70
+ * most current flow.
71
+ */
72
+ const CREATE_PURPOSES = new Set([
73
+ "assistants",
74
+ "batch",
75
+ "fine-tune",
76
+ "vision",
77
+ "user_data",
78
+ "evals"
79
+ ]);
80
+ /**
81
+ * Exposed for tests and for callers that want to check a value themselves:
82
+ * the enum this mock enforces on upload.
83
+ */
84
+ const FILE_CREATE_PURPOSES = [...CREATE_PURPOSES];
85
+ function invalidPurpose(purpose) {
86
+ return `Invalid purpose '${purpose}'. Expected one of: ${[...CREATE_PURPOSES].join(", ")}`;
87
+ }
88
+ /**
89
+ * `Content-Transfer-Encoding` values that are identity transforms (RFC 2045
90
+ * §6.2): the part body already *is* the file's octets, so storing it verbatim
91
+ * is correct. Everything else (`base64`, `quoted-printable`, …) would have to
92
+ * be decoded before storage, and is rejected instead — see
93
+ * {@link unsupportedPartEncoding}.
94
+ */
95
+ const IDENTITY_TRANSFER_ENCODINGS = new Set([
96
+ "7bit",
97
+ "8bit",
98
+ "binary",
99
+ "identity"
100
+ ]);
101
+ /**
102
+ * Reject, rather than silently mis-store, a file part that asks for a content
103
+ * encoding this mock does not apply.
104
+ *
105
+ * We deliberately do NOT implement a decoder. RFC 7578 §4.7 deprecates
106
+ * `Content-Transfer-Encoding` in `multipart/form-data` ("Senders SHOULD NOT
107
+ * generate any parts with a Content-Transfer-Encoding header field. Currently,
108
+ * no deployed implementations that send such bodies have been discovered"), and
109
+ * a capture of the clients that actually reach this endpoint — curl `-F`,
110
+ * python `requests`, python `httpx` (the OpenAI Python SDK's transport) and
111
+ * undici `FormData` (the OpenAI Node SDK's transport) — confirms none of them
112
+ * emits one. Storing an encoded body as-is is the harmful outcome: `bytes` and
113
+ * `GET /content` would report the *encoded* form and a hash comparison in a
114
+ * test would fail for a reason the caller cannot see. A loud 400 naming the
115
+ * header is the honest answer for a mock.
116
+ *
117
+ * `identity` counts as a no-op alongside the RFC 2045 §6.2 values: it is the
118
+ * registered `Content-Encoding` for "no transformation", and a client that
119
+ * spells it on `Content-Transfer-Encoding` means the same thing. Rejecting it
120
+ * named a problem that did not exist.
121
+ *
122
+ * Every part is checked, not only the payload part, and the message names the
123
+ * part it came from. While only the payload part was checked, a `base64`
124
+ * `purpose` part slid through to the enum check and 400'd with
125
+ * `Invalid purpose 'YXNzaXN0YW50cw=='`, pointing the caller at the wrong thing.
126
+ */
127
+ function unsupportedPartEncoding(header, value, partName) {
128
+ return `Unsupported ${header}: '${value}' on the multipart '${partName}' part. aimock stores part bodies verbatim and does not decode them.${header === "Content-Transfer-Encoding" ? ` RFC 7578 section 4.7 deprecates ${header} in multipart/form-data.` : ""} Send the raw bytes instead.`;
129
+ }
130
+ /**
131
+ * Content types for the handful of extensions test harnesses actually upload.
132
+ * Anything unrecognised falls back to `application/octet-stream` — the safe
133
+ * default for opaque bytes, and what stops a PNG being served (and re-decoded)
134
+ * as UTF-8 text.
135
+ *
136
+ * This is an allowlist of INERT types on purpose. `text/html` is deliberately
137
+ * absent: the mock serves uploaded bytes back from its own origin, so any
138
+ * active type here would let a stored upload script that origin in a browser.
139
+ * HTML uploads round-trip byte-for-byte, they are just served as opaque bytes.
140
+ */
141
+ const CONTENT_TYPE_BY_EXT = {
142
+ json: "application/json",
143
+ jsonl: "application/jsonl",
144
+ txt: "text/plain; charset=utf-8",
145
+ md: "text/markdown; charset=utf-8",
146
+ csv: "text/csv; charset=utf-8",
147
+ pdf: "application/pdf",
148
+ png: "image/png",
149
+ jpg: "image/jpeg",
150
+ jpeg: "image/jpeg",
151
+ gif: "image/gif",
152
+ webp: "image/webp",
153
+ wav: "audio/wav",
154
+ mp3: "audio/mpeg",
155
+ zip: "application/zip"
156
+ };
157
+ function contentTypeForFilename(filename) {
158
+ const dot = filename.lastIndexOf(".");
159
+ if (dot === -1) return "application/octet-stream";
160
+ return CONTENT_TYPE_BY_EXT[filename.slice(dot + 1).toLowerCase()] ?? "application/octet-stream";
161
+ }
162
+ /**
163
+ * RFC 5987 §3.2.1 `ext-value` percent-encoding for the `filename*` parameter.
164
+ *
165
+ * `encodeURIComponent` leaves `!'()*-._~` unescaped, but `'`, `(`, `)` and `*`
166
+ * are NOT in RFC 5987's `attr-char` set, so they have to be escaped by hand.
167
+ */
168
+ function encodeExtValue(value) {
169
+ return encodeURIComponent(value).replace(/['()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
170
+ }
171
+ /**
172
+ * `Content-Disposition` for the content route (RFC 6266 §4.1).
173
+ *
174
+ * Always `attachment`. {@link CONTENT_TYPE_BY_EXT} is an allowlist of INERT
175
+ * types precisely so a stored upload cannot execute against the mock's own
176
+ * origin, but `application/pdf` was still being served with no disposition at
177
+ * all, which in every mainstream browser means "render in the built-in PDF
178
+ * viewer" — an active surface on this origin, exactly what the allowlist's
179
+ * comment claims is excluded. `attachment` closes that without touching the
180
+ * allowlist, and it also stops a `.json`/`.txt`/`.csv` body being rendered
181
+ * inline if someone opens a content URL in a tab.
182
+ *
183
+ * What the REAL API sends here is UNVERIFIED: probing
184
+ * `api.openai.com/v1/files/{id}/content` needs a key, and neither
185
+ * `openai/openai-openapi` nor the SDK documents the response headers. So this
186
+ * is chosen for safety, not fidelity — and it is inert for SDK callers: the
187
+ * vendored SDK's `files.content()` (openai@4.104.0,
188
+ * `resources/files.js:82-88`) issues the GET with `Accept: application/binary`
189
+ * and `__binaryResponse: true`, handing back the raw `Response` without ever
190
+ * reading `Content-Disposition`.
191
+ *
192
+ * Both parameters are built from the STORED filename, which is attacker-shaped
193
+ * input: a multipart `filename="..."` can carry CR/LF and would otherwise
194
+ * inject response headers. The ASCII fallback replaces every non-printable and
195
+ * non-ASCII byte with `_` and backslash-escapes `"` and `\` per the
196
+ * quoted-string rule; `filename*` percent-encodes them. Neither can emit a bare
197
+ * CR or LF.
198
+ */
199
+ function contentDispositionFor(filename) {
200
+ const base = `attachment; filename="${filename.replace(/[^\x20-\x7e]/g, "_").replace(/(["\\])/g, "\\$1")}"`;
201
+ if (/^[\x20-\x7e]*$/.test(filename)) return base;
202
+ return `${base}; filename*=UTF-8''${encodeExtValue(filename)}`;
203
+ }
204
+ /**
205
+ * Per-file content cap. An upload whose decoded content is above this gets a
206
+ * `400` with the usual OpenAI-style error envelope, at every wire size a body
207
+ * can have — see {@link FILES_BODY_MAX_BYTES} for how that is kept true
208
+ * without buffering an unbounded body.
209
+ */
210
+ const FILES_MAX_BYTES = 10 * 1024 * 1024;
211
+ /**
212
+ * Worst-case wire-to-content expansion of a JSON upload.
213
+ *
214
+ * `content` is a JSON string, so one byte of decoded payload can arrive as as
215
+ * many as six wire bytes: a `\uXXXX` escape is 6 bytes and the cheapest code
216
+ * point one can carry (U+0000–U+007F) is a single UTF-8 byte. Every other
217
+ * code point does better — U+0800–U+FFFF is 3 UTF-8 bytes per 6 wire bytes,
218
+ * a surrogate pair 4 per 12 — and a multipart part is 1:1 plus its envelope.
219
+ * So six is an upper bound on wire → content expansion for both parsers.
220
+ */
221
+ const FILES_MAX_WIRE_EXPANSION = 6;
222
+ /** Slack for the JSON envelope, or for multipart boundaries and part headers. */
223
+ const FILES_ENVELOPE_HEADROOM_BYTES = 64 * 1024;
224
+ /**
225
+ * How much of a Files upload body the route will BUFFER — derived, not picked.
226
+ *
227
+ * `readBodyBuffer` enforces a limit by destroying the socket: no status line,
228
+ * no body, no CORS headers, just an `ECONNRESET`. That made the `400 File
229
+ * content exceeds ... byte cap` branches below unreachable when the two caps
230
+ * were equal (both the 10 MB `DEFAULT_MAX_BODY_BYTES`), and a fixed slice of
231
+ * headroom above the cap only moved the cliff: a `\u0001`-heavy JSON body or a
232
+ * multipart body far enough over the cap still died on the socket.
233
+ *
234
+ * So the bound is computed from the widest a within-cap payload can be on the
235
+ * wire ({@link FILES_MAX_WIRE_EXPANSION} × the cap, plus envelope headroom).
236
+ * Two consequences, and they are the contract:
237
+ *
238
+ * - No body that could decode to a payload within the cap is ever refused for
239
+ * its size — it fits here by construction, and is answered on content.
240
+ * - A body larger than this is not buffered. The route stops retaining bytes
241
+ * (peak memory stays at this bound), drains the rest, and answers a real
242
+ * `400` with CORS. It is not a socket drop, and it cannot be a false
243
+ * rejection either, by the point above.
244
+ *
245
+ * The global `readBodyBuffer` default is deliberately left alone — every other
246
+ * route depends on it.
247
+ */
248
+ const FILES_BODY_MAX_BYTES = FILES_MAX_BYTES * FILES_MAX_WIRE_EXPANSION + FILES_ENVELOPE_HEADROOM_BYTES;
249
+ /**
250
+ * The one documented socket drop on this route: a DoS backstop.
251
+ *
252
+ * Draining an over-size body is what buys the caller a real `400`, but the
253
+ * draining itself is work an attacker can ask for without limit. Past this
254
+ * the request is simply not worth finishing and the socket is destroyed, the
255
+ * same way every other route treats its own limit. It is deliberately far
256
+ * above {@link FILES_BODY_MAX_BYTES}: a client that lands here is not sending
257
+ * a file that was ever going to be accepted.
258
+ */
259
+ const FILES_BODY_DRAIN_MAX_BYTES = FILES_BODY_MAX_BYTES * 2;
260
+ /**
261
+ * Stands in for an upload body that went past {@link FILES_BODY_MAX_BYTES} and
262
+ * was therefore drained instead of buffered. The route hands this to
263
+ * {@link handleFilesCreate} in place of the bytes, so the over-size answer is
264
+ * the same `400` envelope — journalled, CORS-headed, chaos-gated — as every
265
+ * other rejection here, rather than a second error path in the server.
266
+ */
267
+ const FILES_BODY_OVERSIZED = Symbol("files-body-oversized");
268
+ /** The `400` for a body too large to be a within-cap upload. */
269
+ function oversizedBody() {
270
+ return `Request body exceeds the ${FILES_BODY_MAX_BYTES} byte upload limit for this route (the ${FILES_MAX_BYTES} byte content cap plus worst-case wire encoding overhead)`;
271
+ }
272
+ const fileStore = /* @__PURE__ */ new Map();
273
+ const fileContents = /* @__PURE__ */ new Map();
274
+ function clearFileStore() {
275
+ fileStore.clear();
276
+ fileContents.clear();
277
+ }
278
+ /** The original upload name, without exposing mutable file metadata. */
279
+ function getStoredFileName(id) {
280
+ return fileStore.get(id)?.filename;
281
+ }
282
+ /**
283
+ * The stored bytes for one file, as the very `Buffer` this module holds — so a
284
+ * test can assert on what it RETAINS (`buffer.byteLength`), not just on what it
285
+ * serves back. `GET /v1/files/{id}/content` copies onto the wire, so it cannot
286
+ * tell an owned buffer from a `subarray` pinning a 60 MB request body.
287
+ */
288
+ function getStoredFileBytes(id) {
289
+ return fileContents.get(id)?.bytes;
290
+ }
291
+ /**
292
+ * Register one file in the store and return its wire object. This is the one
293
+ * insert path: `POST /v1/files` uses it, and so do server-side producers of
294
+ * files (the Batches mock minting `output_file_id` / `error_file_id`), so a
295
+ * file minted internally is retrievable through `GET /v1/files/{id}` and
296
+ * `/content` exactly like an upload. `content` must be an OWNED buffer (see
297
+ * {@link ParsedUpload.content}) — it is stored as-is, and it must already be
298
+ * within {@link FILES_MAX_BYTES}: the cap the upload route enforces holds for
299
+ * every file in the store, so a producer that cannot fit under it fails its
300
+ * own operation instead of storing the file.
301
+ */
302
+ function storeFile(file) {
303
+ if (file.content.length > FILES_MAX_BYTES) throw new RangeError(`File content exceeds ${FILES_MAX_BYTES} byte cap`);
304
+ const id = require_helpers.generateId("file");
305
+ const obj = {
306
+ id,
307
+ object: "file",
308
+ bytes: file.content.length,
309
+ created_at: Math.floor(Date.now() / 1e3),
310
+ filename: file.filename,
311
+ purpose: file.purpose,
312
+ status: "processed"
313
+ };
314
+ fileStore.set(id, obj);
315
+ fileContents.set(id, { bytes: file.content });
316
+ return obj;
317
+ }
318
+ /**
319
+ * Synthetic journal body for an upload — the SAME convention
320
+ * {@link handleTranscription} uses for `multipart/form-data` audio: the entry
321
+ * carries a small object describing the upload that was parsed out of the
322
+ * body, never the body itself. Raw file octets are NEVER journalled, in any
323
+ * encoding: they can be megabytes of binary, they would blow the journal's
324
+ * 64 KB body cap on anything real, and a base64 round-trip of them is not
325
+ * something a test wants to diff. `sha256` is the byte identity a caller
326
+ * asserts on instead.
327
+ *
328
+ * `content_type` is derived from the stored filename via
329
+ * {@link contentTypeForFilename} — the same decision `GET /v1/files/{id}/content`
330
+ * serves the bytes back with — rather than from the client-declared part
331
+ * header, which this mock deliberately ignores everywhere else.
332
+ *
333
+ * `model`/`messages` are the inert `ChatCompletionRequest` floor the journal's
334
+ * body type requires (the same `{ model: "", messages: [] }` the server uses
335
+ * for its own bodyless synthetic entries); `_endpointType` names the surface.
336
+ */
337
+ function filesUploadBody(upload) {
338
+ return {
339
+ model: "",
340
+ messages: [],
341
+ _endpointType: "files",
342
+ purpose: upload.purpose,
343
+ filename: upload.filename,
344
+ bytes: upload.content.length,
345
+ content_type: contentTypeForFilename(upload.filename),
346
+ sha256: (0, node_crypto.createHash)("sha256").update(upload.content).digest("hex")
347
+ };
348
+ }
349
+ /**
350
+ * Journal one files-route request.
351
+ *
352
+ * The rule for `body`: an entry carries the {@link filesUploadBody} descriptor
353
+ * exactly when a request had an upload payload that parsed — i.e. a successful
354
+ * `POST /v1/files` — and the {@link invalidRequest} envelope exactly when the
355
+ * request was rejected with a `400` (see {@link rejectFiles}), so the reason
356
+ * is readable from the journal without the wire response. Every other files
357
+ * entry (GET one, GET content, list, DELETE, a chaos-faulted request, a 404)
358
+ * has no payload to describe and stays `null`, matching the bodyless routes in
359
+ * {@link handleTranscription}. Raw uploaded bytes never appear.
360
+ *
361
+ * `service: "files"` is set on every branch so
362
+ * `GET /__aimock/journal?service=files` selects exactly this traffic, and
363
+ * `source: "internal"` because this store is aimock's own — the same tag every
364
+ * fine-tuning entry carries, success and error alike.
365
+ *
366
+ * Call it AFTER the response has been written: a `writeHead` that throws must
367
+ * not leave behind an entry for a response the client never received.
368
+ */
369
+ function journalFiles(journal, method, path, headers, status, body = null) {
370
+ journal.add({
371
+ method,
372
+ path,
373
+ headers,
374
+ body,
375
+ service: "files",
376
+ response: {
377
+ status,
378
+ fixture: null,
379
+ source: "internal"
380
+ }
381
+ });
382
+ }
383
+ /** Keep the entire warning bounded and on one physical line, including the URL. */
384
+ function filesRejectionWarning(message) {
385
+ const maxLength = 1024;
386
+ let safe = "";
387
+ for (const character of message) {
388
+ const code = character.charCodeAt(0);
389
+ const escaped = code < 32 || code >= 127 && code <= 159 || code === 8232 || code === 8233 ? `\\u${code.toString(16).padStart(4, "0")}` : character;
390
+ if (safe.length + escaped.length > maxLength - 3) return `${safe}...`;
391
+ safe += escaped;
392
+ }
393
+ return safe;
394
+ }
395
+ /**
396
+ * Answer a files-route `400`: log it at `warn` (the level the server uses for
397
+ * a request it refused), journal the {@link invalidRequest} envelope as the
398
+ * entry's body so the reason survives in the journal, and write that same
399
+ * envelope to the wire. One helper so the three can never disagree.
400
+ */
401
+ function rejectFiles(res, journal, logger, method, path, headers, message, setCorsHeaders) {
402
+ const envelope = invalidRequest(message);
403
+ logger.warn(filesRejectionWarning(`Files mock: rejected ${method} ${path} with 400: ${message}`));
404
+ writeJson(res, 400, envelope, setCorsHeaders);
405
+ journalFiles(journal, method, path, headers, 400, envelope);
406
+ }
407
+ function writeJson(res, status, payload, setCorsHeaders) {
408
+ setCorsHeaders(res);
409
+ res.writeHead(status, { "Content-Type": "application/json" });
410
+ res.end(JSON.stringify(payload));
411
+ }
412
+ function invalidRequest(message) {
413
+ return { error: {
414
+ message,
415
+ type: "invalid_request_error"
416
+ } };
417
+ }
418
+ /**
419
+ * Name a rejected value's JSON type for the 400 message. The point is that the
420
+ * caller sees *what* they sent instead of silently getting `String(value)`
421
+ * stored as the file's content.
422
+ *
423
+ * The listed cases are exhaustive for the one caller. `JSON.parse` only ever
424
+ * yields null, boolean, number, string, array or plain object — never
425
+ * `undefined`, `function`, `symbol` or `bigint` — and the caller has already
426
+ * excluded `string` (it is the accepted type) and `undefined` (that is the
427
+ * separate "required" 400). So everything reaching the last line is an object.
428
+ */
429
+ function jsonTypeOf(value) {
430
+ if (value === null) return "null";
431
+ if (Array.isArray(value)) return "an array";
432
+ if (typeof value === "number") return "a number";
433
+ if (typeof value === "boolean") return "a boolean";
434
+ return "an object";
435
+ }
436
+ /**
437
+ * A high surrogate not followed by a low one, or a low surrogate not preceded
438
+ * by a high one — i.e. a JS string that is not well-formed UTF-16 and therefore
439
+ * has no UTF-8 encoding.
440
+ *
441
+ * `Buffer.from(s, "utf8")` does not fail on these: it substitutes U+FFFD (EF BF
442
+ * BD), silently and permanently. That breaks the one guarantee an upload mock
443
+ * owes its caller — `GET /v1/files/{id}/content` returns what was uploaded — and
444
+ * it breaks it invisibly, three bytes at a time, in a test whose hash
445
+ * comparison then fails for no visible reason. The byte-for-byte promise this
446
+ * module documents is made for the multipart path (which is scanned as latin1
447
+ * and never decoded); the JSON path carries *text*, so text that cannot be
448
+ * encoded is a 400, not a lossy store.
449
+ */
450
+ const LONE_SURROGATE_RE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
451
+ /**
452
+ * Longest `filename` this mock will store, in UTF-8 bytes.
453
+ *
454
+ * NOT the real API's limit: `openai/openai-openapi` gives `OpenAIFile.filename`
455
+ * as a bare `type: string` with no `maxLength`, and `CreateFileRequest` has no
456
+ * `filename` property at all. So the bound comes from the one thing here a
457
+ * filename can break: `GET /v1/files/{id}/content` echoes the stored name into
458
+ * `Content-Disposition` twice — quoted ASCII (where `"` and `\` double) and
459
+ * percent-encoded per RFC 5987 (where a byte can triple) — so ~5 bytes of
460
+ * header per byte of name. 1024 bytes of name is ~5 KiB of header, well inside
461
+ * Node's 16 KiB default `--max-http-header-size`.
462
+ *
463
+ * Unbounded, a 20 KB filename uploaded `200` and then made the content route
464
+ * unreadable by undici: `UND_ERR_HEADERS_OVERFLOW`, no status, no envelope, no
465
+ * CORS — the OpenAI SDK could not fetch a file it had just uploaded.
466
+ */
467
+ const FILES_MAX_FILENAME_BYTES = 1024;
468
+ /**
469
+ * The filename rules, in one place because both parsers must answer the same.
470
+ *
471
+ * They used to disagree: a name carrying NUL, CR or LF was a `400` as a
472
+ * multipart parameter and a `200` as a JSON field, stored and echoed back
473
+ * verbatim. Returns an error message, or `undefined` if the name is fine.
474
+ */
475
+ function filenameError(filename) {
476
+ if (CONTROL_CHARS.test(filename)) return "Invalid parameter: 'filename' must not contain control characters";
477
+ if (LONE_SURROGATE_RE.test(filename)) return "Invalid parameter: 'filename' must not contain an unpaired UTF-16 surrogate";
478
+ const byteLength = Buffer.byteLength(filename, "utf8");
479
+ if (byteLength > FILES_MAX_FILENAME_BYTES) return `Invalid parameter: 'filename' is ${byteLength} UTF-8 bytes, above the ${FILES_MAX_FILENAME_BYTES} byte cap (a longer name makes GET /v1/files/{id}/content unfetchable: its Content-Disposition header would exceed the HTTP header limit)`;
480
+ }
481
+ /** The deterministic name given to an upload that arrived without one. */
482
+ function synthesizedFilename(content) {
483
+ return `upload-${(0, node_crypto.createHash)("sha256").update(content).digest("hex").slice(0, 24)}.bin`;
484
+ }
485
+ function parseJsonUpload(raw) {
486
+ let text;
487
+ try {
488
+ text = new TextDecoder("utf-8", { fatal: true }).decode(raw);
489
+ } catch {
490
+ return { error: "Invalid request: body is not valid UTF-8" };
491
+ }
492
+ let body;
493
+ try {
494
+ body = JSON.parse(text);
495
+ } catch (err) {
496
+ return { error: `Malformed JSON: ${err instanceof Error ? err.message : "unknown"}` };
497
+ }
498
+ if (!require_helpers.isJsonObject(body)) return { error: "Request body must be a JSON object" };
499
+ const rawFilename = body["filename"];
500
+ const purpose = body["purpose"];
501
+ if (typeof rawFilename !== "string") return { error: "Invalid parameter: 'filename' must be a string" };
502
+ const filenameProblem = filenameError(rawFilename);
503
+ if (filenameProblem !== void 0) return { error: filenameProblem };
504
+ const filename = rawFilename.trim() === "" ? void 0 : rawFilename;
505
+ if (typeof purpose !== "string" || purpose.length === 0) return { error: "Invalid parameter: 'purpose' must be a non-empty string" };
506
+ if (!CREATE_PURPOSES.has(purpose)) return { error: invalidPurpose(purpose) };
507
+ const hasContent = body["content"] !== void 0;
508
+ const hasBytesAlias = body["bytes"] !== void 0;
509
+ if (hasContent && hasBytesAlias) return { error: "Invalid parameter: 'content' and its alias 'bytes' are mutually exclusive; send exactly one" };
510
+ const field = hasContent ? "content" : "bytes";
511
+ const content = hasContent ? body["content"] : body["bytes"];
512
+ if (content === void 0) return { error: "Invalid parameter: 'content' is required (or its alias 'bytes')" };
513
+ if (typeof content !== "string") return { error: `Invalid parameter: '${field}' must be a string, got ${jsonTypeOf(content)}` };
514
+ if (LONE_SURROGATE_RE.test(content)) return { error: `Invalid parameter: '${field}' contains an unpaired UTF-16 surrogate and is not valid text. aimock would have to store it as U+FFFD, so the bytes served back by GET /v1/files/{id}/content would not be the bytes you sent. Send binary as a multipart/form-data upload, which is stored verbatim.` };
515
+ const bytes = Buffer.from(content, "utf8");
516
+ if (bytes.length > FILES_MAX_BYTES) return { error: `File content exceeds ${FILES_MAX_BYTES} byte cap` };
517
+ return {
518
+ filename: filename ?? synthesizedFilename(bytes),
519
+ purpose,
520
+ content: bytes
521
+ };
522
+ }
523
+ const HEADER_TERMINATOR = Buffer.from("\r\n\r\n");
524
+ /** RFC 9110 `token` characters — the legal set for a header field name. */
525
+ const TOKEN_CHAR = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
526
+ /** C0 controls plus DEL — never legitimate in a filename we echo back. */
527
+ const CONTROL_CHARS = /[-]/;
528
+ /**
529
+ * Is this request body `multipart/form-data`?
530
+ *
531
+ * Compares the media type's *essence* (RFC 9110 §8.3.1: everything before the
532
+ * first `;`), not a substring of the whole header. A substring test routed
533
+ * `application/json; note="multipart/form-data"` — and anything else that
534
+ * merely mentions the token in a parameter — into the multipart parser, which
535
+ * then 400'd about a missing `boundary` directive on a perfectly good JSON
536
+ * body.
537
+ */
538
+ function isMultipartFormData(contentType) {
539
+ if (contentType === void 0) return false;
540
+ const semi = contentType.indexOf(";");
541
+ return (semi === -1 ? contentType : contentType.slice(0, semi)).trim().toLowerCase() === "multipart/form-data";
542
+ }
543
+ /**
544
+ * Locate the next *boundary delimiter line* at or after `from`.
545
+ *
546
+ * RFC 2046 §5.1.1 defines the delimiter as `CRLF "--" boundary` followed by
547
+ * either transport-padding + CRLF (another part follows) or `--` (the close
548
+ * delimiter). Both halves matter:
549
+ *
550
+ * - The **leading CRLF is part of the delimiter**, not of the preceding body.
551
+ * The previous implementation split on a bare `--boundary`, so a payload
552
+ * whose bytes happened to contain that sequence was truncated at it and
553
+ * stored short — with a 200, silently, which is the worst possible answer
554
+ * from a module whose contract is byte-for-byte storage.
555
+ * - An occurrence **not followed by** CRLF or `--` is not a delimiter line at
556
+ * all, so it is ordinary body content and the scan continues past it. That
557
+ * is what keeps a payload containing `CRLF--boundary` plus arbitrary bytes
558
+ * byte-exact. (undici's `Response.formData()` rejects such a body outright;
559
+ * see the note on {@link parseMultipartUpload}.)
560
+ */
561
+ function nextDelimiter(buf, delimiter, from) {
562
+ let at = buf.indexOf(delimiter, from);
563
+ while (at !== -1) {
564
+ const tail = delimiterTail(buf, at + delimiter.length);
565
+ if (tail) return {
566
+ at,
567
+ ...tail
568
+ };
569
+ at = buf.indexOf(delimiter, at + 1);
570
+ }
571
+ }
572
+ /**
573
+ * Classify what follows a `CRLF "--" boundary` match: transport-padding + CRLF
574
+ * (another part follows), `--` (the close delimiter), or neither — in which
575
+ * case the match is not a delimiter line at all and the caller keeps scanning.
576
+ * Split out of {@link nextDelimiter} so {@link parseMultipartUpload} can apply
577
+ * the same rule to an opening delimiter whose leading CRLF is not present.
578
+ */
579
+ function delimiterTail(buf, after) {
580
+ if (buf[after] === 45 && buf[after + 1] === 45) return {
581
+ next: after + 2,
582
+ closing: true
583
+ };
584
+ let j = after;
585
+ while (buf[j] === 32 || buf[j] === 9) j += 1;
586
+ if (buf[j] === 13 && buf[j + 1] === 10) return {
587
+ next: j + 2,
588
+ closing: false
589
+ };
590
+ }
591
+ function parseHeaderParams(value) {
592
+ const params = /* @__PURE__ */ new Map();
593
+ const semi = value.indexOf(";");
594
+ const head = (semi === -1 ? value : value.slice(0, semi)).trim();
595
+ let i = semi === -1 ? value.length : semi;
596
+ while (i < value.length) {
597
+ i += 1;
598
+ while (value[i] === " " || value[i] === " ") i += 1;
599
+ if (i >= value.length) break;
600
+ const eq = value.indexOf("=", i);
601
+ if (eq === -1) return MALFORMED_HEADER;
602
+ const attr = value.slice(i, eq).trim().toLowerCase();
603
+ if (!TOKEN_CHAR.test(attr)) return MALFORMED_HEADER;
604
+ if (params.has(attr)) return {
605
+ error: "duplicate",
606
+ param: attr
607
+ };
608
+ i = eq + 1;
609
+ if (value[i] === "\"") {
610
+ i += 1;
611
+ let out = "";
612
+ for (;;) {
613
+ if (i >= value.length) return MALFORMED_HEADER;
614
+ const ch = value[i];
615
+ if (ch === "\\" && i + 1 < value.length) {
616
+ out += value[i + 1];
617
+ i += 2;
618
+ continue;
619
+ }
620
+ if (ch === "\"") {
621
+ i += 1;
622
+ break;
623
+ }
624
+ out += ch;
625
+ i += 1;
626
+ }
627
+ params.set(attr, out);
628
+ while (value[i] === " " || value[i] === " ") i += 1;
629
+ if (i < value.length && value[i] !== ";") return MALFORMED_HEADER;
630
+ } else {
631
+ const end = value.indexOf(";", i);
632
+ params.set(attr, (end === -1 ? value.slice(i) : value.slice(i, end)).trim());
633
+ i = end === -1 ? value.length : end;
634
+ }
635
+ }
636
+ return {
637
+ value: head,
638
+ params
639
+ };
640
+ }
641
+ const MALFORMED_HEADER = { error: "malformed" };
642
+ /** Render a {@link parseHeaderParams} failure as a 400 message naming the header. */
643
+ function headerParamError(err, header, where) {
644
+ return err.error === "duplicate" ? `Invalid request: duplicate '${err.param}' parameter on the ${header} header in ${where}` : `Invalid request: malformed ${header} header in ${where}`;
645
+ }
646
+ /**
647
+ * Decode an RFC 5987 `ext-value` (`charset'language'pct-encoded`), the form a
648
+ * `filename*` parameter takes. RFC 6266 §4.3 says a recipient that understands
649
+ * it SHOULD prefer it over the plain `filename`, so {@link parsePart} does.
650
+ * A client that sends only `filename*=` therefore keeps the name it sent
651
+ * instead of receiving a synthesized one.
652
+ */
653
+ function decodeExtValue(raw) {
654
+ const first = raw.indexOf("'");
655
+ const second = raw.indexOf("'", first + 1);
656
+ if (first === -1 || second === -1) return void 0;
657
+ const charset = raw.slice(0, first).toLowerCase();
658
+ if (charset !== "utf-8" && charset !== "iso-8859-1") return void 0;
659
+ const octets = [];
660
+ const pct = raw.slice(second + 1);
661
+ for (let i = 0; i < pct.length; i += 1) if (pct[i] === "%") {
662
+ const hex = pct.slice(i + 1, i + 3);
663
+ if (!/^[0-9A-Fa-f]{2}$/.test(hex)) return void 0;
664
+ octets.push(parseInt(hex, 16));
665
+ i += 2;
666
+ } else {
667
+ const code = pct.charCodeAt(i);
668
+ if (code > 127) return void 0;
669
+ octets.push(code);
670
+ }
671
+ const bytes = Buffer.from(octets);
672
+ if (charset === "iso-8859-1") return bytes.toString("latin1");
673
+ try {
674
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
675
+ } catch {
676
+ return;
677
+ }
678
+ }
679
+ /** Turn one part's raw octets into headers + body, or name why it is malformed. */
680
+ function parsePart(partBytes, index) {
681
+ const where = `multipart part #${index + 1}`;
682
+ let headerText;
683
+ let bodyStart;
684
+ if (partBytes[0] === 13 && partBytes[1] === 10) {
685
+ headerText = "";
686
+ bodyStart = 2;
687
+ } else {
688
+ const end = partBytes.indexOf(HEADER_TERMINATOR);
689
+ if (end === -1) return { error: `Invalid request: malformed ${where} — its headers are not terminated by a blank line` };
690
+ headerText = partBytes.subarray(0, end).toString("utf8");
691
+ bodyStart = end + 4;
692
+ }
693
+ const headers = /* @__PURE__ */ new Map();
694
+ if (headerText !== "") for (const line of headerText.split("\r\n")) {
695
+ const colon = line.indexOf(":");
696
+ const fieldName = colon === -1 ? "" : line.slice(0, colon);
697
+ if (!TOKEN_CHAR.test(fieldName)) return { error: `Invalid request: malformed header line in ${where}` };
698
+ const field = fieldName.toLowerCase();
699
+ if (headers.has(field)) return { error: `Invalid request: duplicate '${fieldName.trim()}' header in ${where}` };
700
+ headers.set(field, line.slice(colon + 1).trim());
701
+ }
702
+ const disposition = headers.get("content-disposition");
703
+ if (disposition === void 0) return { error: `Invalid request: ${where} has no Content-Disposition header` };
704
+ const parsed = parseHeaderParams(disposition);
705
+ if ("error" in parsed) return { error: headerParamError(parsed, "Content-Disposition", where) };
706
+ if (parsed.value.toLowerCase() !== "form-data") return { error: `Invalid request: ${where} has Content-Disposition type '${parsed.value}'; RFC 7578 section 4.2 requires 'form-data'` };
707
+ const name = parsed.params.get("name");
708
+ if (name === void 0) return { error: `Invalid request: ${where} has no 'name' parameter on its Content-Disposition header` };
709
+ const star = parsed.params.get("filename*");
710
+ const filename = star !== void 0 ? decodeExtValue(star) : parsed.params.get("filename");
711
+ if (star !== void 0 && filename === void 0) return { error: `Invalid request: malformed 'filename*' parameter in ${where}` };
712
+ return {
713
+ name,
714
+ filename,
715
+ headers,
716
+ body: partBytes.subarray(bodyStart)
717
+ };
718
+ }
719
+ /**
720
+ * Read a part's content encoding header. An empty value reads as absent: the
721
+ * old regex used `\s*` after the colon, which matches a CRLF, so an empty
722
+ * `Content-Transfer-Encoding:` captured the *next* header line and 400'd
723
+ * naming `content-type: image/png` as the unsupported encoding.
724
+ */
725
+ function partEncoding(part, header) {
726
+ const value = part.headers.get(header)?.trim().toLowerCase();
727
+ return value === void 0 || value === "" ? void 0 : value;
728
+ }
729
+ /**
730
+ * Parse a `multipart/form-data` upload body.
731
+ *
732
+ * ## Why this is not `Response.formData()`
733
+ *
734
+ * Node's built-in (undici) `Response.formData()` is a spec-compliant multipart
735
+ * parser and was evaluated first, per the "use the platform, do not hand-roll
736
+ * a codec" rule. It fails three of this module's requirements:
737
+ *
738
+ * 1. **Byte fidelity.** It throws `expected CRLF` on any body whose payload
739
+ * contains the boundary token, including the RFC-legal bare `--boundary`
740
+ * with no preceding CRLF. Those bytes are storable and this module promises
741
+ * to store them.
742
+ * 2. **Filename-less payload parts.** A part with no `filename` parameter comes
743
+ * back as a *string*, decoded as UTF-8, so binary octets are destroyed
744
+ * (`00 ff fe ...` becomes `00 efbfbd efbfbd ...`). Such a part is legal
745
+ * input here and already round-tripped byte-exact.
746
+ * 3. **Part headers.** `File` exposes only `name`, `type` and `size`. There is
747
+ * no way to see a part's `Content-Transfer-Encoding`, so the "reject an
748
+ * encoding we do not apply" contract above could not be implemented at all
749
+ * and a `base64` part would be stored in its encoded form — the exact bug
750
+ * that contract exists to prevent.
751
+ *
752
+ * So the framing stays local, but it is written to RFC 2046 §5.1.1 and
753
+ * RFC 7578 rather than to a `split()`: see {@link nextDelimiter} for the
754
+ * delimiter rule and {@link parsePart} for the header rule.
755
+ *
756
+ * Bytes never round-trip through a string. Part bodies are `subarray`s of the
757
+ * request buffer, so what is parsed is literally what arrived on the wire — but
758
+ * the payload part is COPIED out of that view before it is returned, because a
759
+ * `subarray` retains its whole backing buffer (see the copy below). The scan
760
+ * itself never copies the body either, so peak residency during a parse is one
761
+ * request body, not two.
762
+ */
763
+ function parseMultipartUpload(rawBytes, contentType) {
764
+ const parsedType = parseHeaderParams(contentType ?? "");
765
+ if ("error" in parsedType) return { error: headerParamError(parsedType, "Content-Type", "the request") };
766
+ const boundary = parsedType.params.get("boundary");
767
+ if (!boundary) return { error: "Invalid request: multipart/form-data Content-Type is missing a 'boundary' directive" };
768
+ const buf = rawBytes;
769
+ const delimiter = Buffer.from(`\r\n--${boundary}`, "latin1");
770
+ const dashBoundary = Buffer.from(`--${boundary}`, "latin1");
771
+ const opening = buf.subarray(0, dashBoundary.length).equals(dashBoundary) ? delimiterTail(buf, dashBoundary.length) : void 0;
772
+ let delim = opening ? {
773
+ at: 0,
774
+ ...opening
775
+ } : nextDelimiter(buf, delimiter, 0);
776
+ if (!delim) return { error: "Invalid request: multipart body contains no boundary delimiter" };
777
+ const parts = [];
778
+ while (!delim.closing) {
779
+ const start = delim.next;
780
+ const following = nextDelimiter(buf, delimiter, start);
781
+ if (!following) return { error: "Invalid request: multipart body ended before its closing boundary delimiter" };
782
+ const part = parsePart(buf.subarray(start, following.at), parts.length);
783
+ if ("error" in part) return part;
784
+ parts.push(part);
785
+ delim = following;
786
+ }
787
+ for (const part of parts) {
788
+ const transfer = partEncoding(part, "content-transfer-encoding");
789
+ if (transfer !== void 0 && !IDENTITY_TRANSFER_ENCODINGS.has(transfer)) return { error: unsupportedPartEncoding("Content-Transfer-Encoding", transfer, part.name) };
790
+ const encoding = partEncoding(part, "content-encoding");
791
+ if (encoding !== void 0 && encoding !== "identity") return { error: unsupportedPartEncoding("Content-Encoding", encoding, part.name) };
792
+ }
793
+ const purposeParts = parts.filter((p) => p.name === "purpose");
794
+ if (purposeParts.length > 1) return { error: "Invalid request: multipart body has more than one 'purpose' part; send exactly one" };
795
+ const payloadParts = parts.filter((p) => p.name === "file" || p.name === "content");
796
+ if (payloadParts.length > 1) {
797
+ const [first, second] = payloadParts;
798
+ return { error: `Invalid request: multipart body has more than one payload part ('${first.name}' and '${second.name}'); send exactly one 'file' or 'content' part` };
799
+ }
800
+ const purpose = purposeParts[0]?.body.toString("utf8");
801
+ if (!purpose) return { error: "Invalid parameter: multipart 'purpose' field is required" };
802
+ if (!CREATE_PURPOSES.has(purpose)) return { error: invalidPurpose(purpose) };
803
+ const payload = payloadParts[0];
804
+ if (payload === void 0) return { error: "Invalid request: multipart body must include a 'file' part (or its 'content' alias)" };
805
+ if (payload.body.length > FILES_MAX_BYTES) return { error: `File content exceeds ${FILES_MAX_BYTES} byte cap` };
806
+ const content = Buffer.alloc(payload.body.length);
807
+ payload.body.copy(content);
808
+ let filename = payload.filename;
809
+ if (filename !== void 0) {
810
+ const problem = filenameError(filename);
811
+ if (problem !== void 0) return { error: problem };
812
+ }
813
+ if (filename !== void 0 && filename.trim() === "") filename = void 0;
814
+ if (filename === void 0) filename = synthesizedFilename(content);
815
+ return {
816
+ filename,
817
+ purpose,
818
+ content
819
+ };
820
+ }
821
+ async function handleFilesCreate(req, res, raw, journal, defaults, setCorsHeaders) {
822
+ const path = req.url ?? "/v1/files";
823
+ const method = req.method ?? "POST";
824
+ setCorsHeaders(res);
825
+ if (await require_chaos.applyChaosAsync(res, null, defaults.chaos, req.headers, req.url, journal, {
826
+ method,
827
+ path,
828
+ headers: require_helpers.flattenHeaders(req.headers),
829
+ body: null,
830
+ service: "files"
831
+ }, "internal", defaults.registry, defaults.logger)) return;
832
+ if (raw === FILES_BODY_OVERSIZED) {
833
+ rejectFiles(res, journal, defaults.logger, method, path, require_helpers.flattenHeaders(req.headers), oversizedBody(), setCorsHeaders);
834
+ return;
835
+ }
836
+ const contentType = Array.isArray(req.headers["content-type"]) ? req.headers["content-type"][0] : req.headers["content-type"];
837
+ const parsed = isMultipartFormData(contentType) ? parseMultipartUpload(raw, contentType) : parseJsonUpload(raw);
838
+ if ("error" in parsed) {
839
+ rejectFiles(res, journal, defaults.logger, method, path, require_helpers.flattenHeaders(req.headers), parsed.error, setCorsHeaders);
840
+ return;
841
+ }
842
+ const obj = storeFile(parsed);
843
+ const { id, bytes } = obj;
844
+ defaults.logger.debug(`Files mock: stored ${id} (${parsed.filename}, ${bytes} bytes)`);
845
+ writeJson(res, 200, obj, setCorsHeaders);
846
+ journalFiles(journal, method, path, require_helpers.flattenHeaders(req.headers), 200, filesUploadBody(parsed));
847
+ }
848
+ /**
849
+ * `GET /v1/files` paging bounds, taken from the `openai/openai-openapi` spec
850
+ * (`operationId: listFiles`): "Limit can range between 1 and 10,000, and the
851
+ * default is 10,000." Files is the odd one out among the list surfaces — most
852
+ * default to 20 — so the default is spelled out here rather than shared.
853
+ */
854
+ const FILES_LIST_MAX_LIMIT = 1e4;
855
+ const FILES_LIST_DEFAULT_LIMIT = FILES_LIST_MAX_LIMIT;
856
+ /**
857
+ * The single non-empty value of a list query parameter, or the 400 it earns.
858
+ *
859
+ * ONE rule for all four of `limit`, `order`, `after` and `purpose`, because the
860
+ * three ways of reading them used to disagree on the same URL:
861
+ *
862
+ * - REPEATED is a 400. `URLSearchParams.get` answers with the FIRST of a
863
+ * repeated parameter and launders the rest, so `?limit=1&limit=abc` paged
864
+ * at 1 and never looked at `abc` — a caller that built its URL twice over
865
+ * got a silent success for a value it never meant. `listFiles` declares
866
+ * each of the four as a single scalar and gives no combining rule for a
867
+ * repeat, so a repeat is a caller error. Matches the multipart body, where
868
+ * two `purpose` parts are already a 400 rather than a last-one-wins.
869
+ * - PRESENT-BUT-EMPTY is a 400. `?order=` was already one (no member of the
870
+ * enum is ""), but `?after=` meant "no cursor" and `?purpose=` meant "no
871
+ * filter" and handed back the WHOLE store — the exact opposite of the
872
+ * narrowing the caller asked for. An empty string is not an object id and
873
+ * not a purpose any stored file can carry, and `?x=` is what building a
874
+ * URL from a partly-undefined params object emits, which is a bug worth
875
+ * showing. Omitting the parameter is how you say "no filter".
876
+ * - ABSENT is `null`, and each caller applies its own default.
877
+ *
878
+ * Both 400s are OURS, not the vendor's: the spec states neither. They are the
879
+ * mock's standing bias towards failing loudly over answering a question the
880
+ * caller did not ask.
881
+ */
882
+ function singleParam(params, name) {
883
+ const all = params.getAll(name);
884
+ if (all.length > 1) return {
885
+ ok: false,
886
+ error: `Invalid parameter: '${name}' was given ${all.length} times; it takes a single value`
887
+ };
888
+ if (all.length === 0) return {
889
+ ok: true,
890
+ value: null
891
+ };
892
+ if (all[0] === "") return {
893
+ ok: false,
894
+ error: `Invalid parameter: '${name}' was given an empty value; omit it instead`
895
+ };
896
+ return {
897
+ ok: true,
898
+ value: all[0]
899
+ };
900
+ }
901
+ /**
902
+ * Parse the `limit` / `order` / `after` / `purpose` parameters off a list URL.
903
+ *
904
+ * Shapes come from `operationId: listFiles` in `openai/openai-openapi` and from
905
+ * `FileListParams extends CursorPageParams` in the vendored SDK (openai
906
+ * 4.104.0 — `resources/files.d.ts` gives `order?: 'asc' | 'desc'` and
907
+ * `purpose?: string`; `pagination.d.ts` gives `after?: string` and
908
+ * `limit?: number`). `order` defaults to `desc`, so the newest file comes back
909
+ * first.
910
+ *
911
+ * All four are read here, through {@link singleParam}, so there is exactly one
912
+ * place that decides what a repeated or empty list parameter means. `purpose`
913
+ * used to be read separately in `src/server.ts` and passed in already parsed,
914
+ * which is how it ended up with neither rule.
915
+ */
916
+ function parseListQuery(rawUrl) {
917
+ const queryStart = rawUrl.indexOf("?");
918
+ const params = new URLSearchParams(queryStart === -1 ? "" : rawUrl.slice(queryStart + 1));
919
+ const rawOrder = singleParam(params, "order");
920
+ if (!rawOrder.ok) return { error: rawOrder.error };
921
+ const orderRaw = rawOrder.value;
922
+ if (orderRaw !== null && orderRaw !== "asc" && orderRaw !== "desc") return { error: "Invalid parameter: 'order' must be one of: asc, desc" };
923
+ const rawLimit = singleParam(params, "limit");
924
+ if (!rawLimit.ok) return { error: rawLimit.error };
925
+ let limit = FILES_LIST_DEFAULT_LIMIT;
926
+ if (rawLimit.value !== null) {
927
+ const parsed = require_helpers.parseStrictIntegerText(rawLimit.value);
928
+ if (parsed === null || parsed < 1 || parsed > FILES_LIST_MAX_LIMIT) return { error: `Invalid parameter: 'limit' must be an integer between 1 and ${FILES_LIST_MAX_LIMIT}` };
929
+ limit = parsed;
930
+ }
931
+ const rawAfter = singleParam(params, "after");
932
+ if (!rawAfter.ok) return { error: rawAfter.error };
933
+ const rawPurpose = singleParam(params, "purpose");
934
+ if (!rawPurpose.ok) return { error: rawPurpose.error };
935
+ return {
936
+ limit,
937
+ order: orderRaw ?? "desc",
938
+ after: rawAfter.value ?? void 0,
939
+ purpose: rawPurpose.value ?? void 0
940
+ };
941
+ }
942
+ async function handleFilesList(req, res, journal, defaults, setCorsHeaders) {
943
+ const path = req.url ?? "/v1/files";
944
+ const method = req.method ?? "GET";
945
+ setCorsHeaders(res);
946
+ if (await require_chaos.applyChaosAsync(res, null, defaults.chaos, req.headers, req.url, journal, {
947
+ method,
948
+ path,
949
+ headers: require_helpers.flattenHeaders(req.headers),
950
+ body: null,
951
+ service: "files"
952
+ }, "internal", defaults.registry, defaults.logger)) return;
953
+ const query = parseListQuery(path);
954
+ if ("error" in query) {
955
+ rejectFiles(res, journal, defaults.logger, method, path, require_helpers.flattenHeaders(req.headers), query.error, setCorsHeaders);
956
+ return;
957
+ }
958
+ let data = [...fileStore.values()];
959
+ if (query.purpose !== void 0) data = data.filter((f) => f.purpose === query.purpose);
960
+ data.sort((a, b) => a.created_at - b.created_at);
961
+ if (query.order === "desc") data.reverse();
962
+ let start = 0;
963
+ if (query.after !== void 0) {
964
+ const idx = data.findIndex((f) => f.id === query.after);
965
+ if (idx === -1) {
966
+ rejectFiles(res, journal, defaults.logger, method, path, require_helpers.flattenHeaders(req.headers), `Invalid parameter: 'after' cursor '${query.after}' does not match any file in this listing`, setCorsHeaders);
967
+ return;
968
+ }
969
+ start = idx + 1;
970
+ }
971
+ const page = data.slice(start, start + query.limit);
972
+ writeJson(res, 200, {
973
+ object: "list",
974
+ data: page,
975
+ first_id: page[0]?.id ?? null,
976
+ last_id: page[page.length - 1]?.id ?? null,
977
+ has_more: start + page.length < data.length
978
+ }, setCorsHeaders);
979
+ journalFiles(journal, method, path, require_helpers.flattenHeaders(req.headers), 200);
980
+ }
981
+ async function handleFilesRetrieve(req, res, fileId, journal, defaults, setCorsHeaders) {
982
+ const path = req.url ?? `/v1/files/${fileId}`;
983
+ const method = req.method ?? "GET";
984
+ setCorsHeaders(res);
985
+ if (await require_chaos.applyChaosAsync(res, null, defaults.chaos, req.headers, req.url, journal, {
986
+ method,
987
+ path,
988
+ headers: require_helpers.flattenHeaders(req.headers),
989
+ body: null,
990
+ service: "files"
991
+ }, "internal", defaults.registry, defaults.logger)) return;
992
+ const found = fileStore.get(fileId);
993
+ if (!found) {
994
+ writeJson(res, 404, invalidRequest(`No such file: ${fileId}`), setCorsHeaders);
995
+ journalFiles(journal, method, path, require_helpers.flattenHeaders(req.headers), 404);
996
+ return;
997
+ }
998
+ writeJson(res, 200, found, setCorsHeaders);
999
+ journalFiles(journal, method, path, require_helpers.flattenHeaders(req.headers), 200);
1000
+ }
1001
+ async function handleFilesContent(req, res, fileId, journal, defaults, setCorsHeaders) {
1002
+ const path = req.url ?? `/v1/files/${fileId}/content`;
1003
+ const method = req.method ?? "GET";
1004
+ setCorsHeaders(res);
1005
+ if (await require_chaos.applyChaosAsync(res, null, defaults.chaos, req.headers, req.url, journal, {
1006
+ method,
1007
+ path,
1008
+ headers: require_helpers.flattenHeaders(req.headers),
1009
+ body: null,
1010
+ service: "files"
1011
+ }, "internal", defaults.registry, defaults.logger)) return;
1012
+ const found = fileStore.get(fileId);
1013
+ const content = fileContents.get(fileId);
1014
+ if (!found || content === void 0) {
1015
+ writeJson(res, 404, invalidRequest(`No such file: ${fileId}`), setCorsHeaders);
1016
+ journalFiles(journal, method, path, require_helpers.flattenHeaders(req.headers), 404);
1017
+ return;
1018
+ }
1019
+ res.writeHead(200, {
1020
+ "Content-Type": contentTypeForFilename(found.filename),
1021
+ "Content-Length": String(content.bytes.length),
1022
+ "X-Content-Type-Options": "nosniff",
1023
+ "Content-Disposition": contentDispositionFor(found.filename)
1024
+ });
1025
+ res.end(content.bytes);
1026
+ journalFiles(journal, method, path, require_helpers.flattenHeaders(req.headers), 200);
1027
+ }
1028
+ async function handleFilesDelete(req, res, fileId, journal, defaults, setCorsHeaders) {
1029
+ const path = req.url ?? `/v1/files/${fileId}`;
1030
+ const method = req.method ?? "DELETE";
1031
+ setCorsHeaders(res);
1032
+ if (await require_chaos.applyChaosAsync(res, null, defaults.chaos, req.headers, req.url, journal, {
1033
+ method,
1034
+ path,
1035
+ headers: require_helpers.flattenHeaders(req.headers),
1036
+ body: null,
1037
+ service: "files"
1038
+ }, "internal", defaults.registry, defaults.logger)) return;
1039
+ if (!fileStore.get(fileId)) {
1040
+ writeJson(res, 404, invalidRequest(`No such file: ${fileId}`), setCorsHeaders);
1041
+ journalFiles(journal, method, path, require_helpers.flattenHeaders(req.headers), 404);
1042
+ return;
1043
+ }
1044
+ fileStore.delete(fileId);
1045
+ fileContents.delete(fileId);
1046
+ require_vector_stores.removeVectorStoreFile(fileId);
1047
+ writeJson(res, 200, {
1048
+ id: fileId,
1049
+ object: "file",
1050
+ deleted: true
1051
+ }, setCorsHeaders);
1052
+ journalFiles(journal, method, path, require_helpers.flattenHeaders(req.headers), 200);
1053
+ }
1054
+
1055
+ //#endregion
1056
+ exports.FILES_BODY_DRAIN_MAX_BYTES = FILES_BODY_DRAIN_MAX_BYTES;
1057
+ exports.FILES_BODY_MAX_BYTES = FILES_BODY_MAX_BYTES;
1058
+ exports.FILES_BODY_OVERSIZED = FILES_BODY_OVERSIZED;
1059
+ exports.FILES_MAX_BYTES = FILES_MAX_BYTES;
1060
+ exports.clearFileStore = clearFileStore;
1061
+ exports.getStoredFileBytes = getStoredFileBytes;
1062
+ exports.getStoredFileName = getStoredFileName;
1063
+ exports.handleFilesContent = handleFilesContent;
1064
+ exports.handleFilesCreate = handleFilesCreate;
1065
+ exports.handleFilesDelete = handleFilesDelete;
1066
+ exports.handleFilesList = handleFilesList;
1067
+ exports.handleFilesRetrieve = handleFilesRetrieve;
1068
+ exports.storeFile = storeFile;
1069
+ //# sourceMappingURL=files.cjs.map