@copilotkit/aimock 1.42.0 → 1.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +56 -0
  4. package/README.md +102 -16
  5. package/dist/agui-mock.cjs +1 -1
  6. package/dist/agui-mock.cjs.map +1 -1
  7. package/dist/agui-mock.d.cts.map +1 -1
  8. package/dist/agui-mock.d.ts.map +1 -1
  9. package/dist/agui-mock.js +1 -1
  10. package/dist/agui-mock.js.map +1 -1
  11. package/dist/agui-recorder.cjs +33 -11
  12. package/dist/agui-recorder.cjs.map +1 -1
  13. package/dist/agui-recorder.d.cts +10 -1
  14. package/dist/agui-recorder.d.cts.map +1 -1
  15. package/dist/agui-recorder.d.ts +10 -1
  16. package/dist/agui-recorder.d.ts.map +1 -1
  17. package/dist/agui-recorder.js +33 -11
  18. package/dist/agui-recorder.js.map +1 -1
  19. package/dist/agui-types.d.cts +1 -0
  20. package/dist/agui-types.d.cts.map +1 -1
  21. package/dist/agui-types.d.ts +1 -0
  22. package/dist/agui-types.d.ts.map +1 -1
  23. package/dist/aimock-cli.cjs +38 -10
  24. package/dist/aimock-cli.cjs.map +1 -1
  25. package/dist/aimock-cli.d.cts +5 -0
  26. package/dist/aimock-cli.d.cts.map +1 -1
  27. package/dist/aimock-cli.d.ts +5 -0
  28. package/dist/aimock-cli.d.ts.map +1 -1
  29. package/dist/aimock-cli.js +38 -10
  30. package/dist/aimock-cli.js.map +1 -1
  31. package/dist/batches.cjs +458 -0
  32. package/dist/batches.cjs.map +1 -0
  33. package/dist/batches.d.cts +57 -0
  34. package/dist/batches.d.cts.map +1 -0
  35. package/dist/batches.d.ts +57 -0
  36. package/dist/batches.d.ts.map +1 -0
  37. package/dist/batches.js +453 -0
  38. package/dist/batches.js.map +1 -0
  39. package/dist/bedrock-converse.cjs +12 -8
  40. package/dist/bedrock-converse.cjs.map +1 -1
  41. package/dist/bedrock-converse.d.cts.map +1 -1
  42. package/dist/bedrock-converse.d.ts.map +1 -1
  43. package/dist/bedrock-converse.js +14 -10
  44. package/dist/bedrock-converse.js.map +1 -1
  45. package/dist/bedrock.cjs +10 -8
  46. package/dist/bedrock.cjs.map +1 -1
  47. package/dist/bedrock.d.cts.map +1 -1
  48. package/dist/bedrock.d.ts.map +1 -1
  49. package/dist/bedrock.js +12 -10
  50. package/dist/bedrock.js.map +1 -1
  51. package/dist/byteplus-video.cjs +3 -3
  52. package/dist/byteplus-video.cjs.map +1 -1
  53. package/dist/byteplus-video.js +4 -4
  54. package/dist/byteplus-video.js.map +1 -1
  55. package/dist/chaos.cjs +622 -72
  56. package/dist/chaos.cjs.map +1 -1
  57. package/dist/chaos.d.cts +164 -7
  58. package/dist/chaos.d.cts.map +1 -1
  59. package/dist/chaos.d.ts +164 -7
  60. package/dist/chaos.d.ts.map +1 -1
  61. package/dist/chaos.js +613 -74
  62. package/dist/chaos.js.map +1 -1
  63. package/dist/cli.cjs +42 -23
  64. package/dist/cli.cjs.map +1 -1
  65. package/dist/cli.js +42 -23
  66. package/dist/cli.js.map +1 -1
  67. package/dist/cohere.cjs +10 -8
  68. package/dist/cohere.cjs.map +1 -1
  69. package/dist/cohere.d.cts.map +1 -1
  70. package/dist/cohere.d.ts.map +1 -1
  71. package/dist/cohere.js +12 -10
  72. package/dist/cohere.js.map +1 -1
  73. package/dist/config-loader.d.cts.map +1 -1
  74. package/dist/config-loader.d.ts.map +1 -1
  75. package/dist/convert.cjs +1 -1
  76. package/dist/convert.cjs.map +1 -1
  77. package/dist/convert.js +1 -1
  78. package/dist/convert.js.map +1 -1
  79. package/dist/elevenlabs-audio.cjs +3 -3
  80. package/dist/elevenlabs-audio.cjs.map +1 -1
  81. package/dist/elevenlabs-audio.js +4 -4
  82. package/dist/elevenlabs-audio.js.map +1 -1
  83. package/dist/elevenlabs-voice.cjs +1053 -0
  84. package/dist/elevenlabs-voice.cjs.map +1 -0
  85. package/dist/elevenlabs-voice.d.cts +72 -0
  86. package/dist/elevenlabs-voice.d.cts.map +1 -0
  87. package/dist/elevenlabs-voice.d.ts +72 -0
  88. package/dist/elevenlabs-voice.d.ts.map +1 -0
  89. package/dist/elevenlabs-voice.js +1048 -0
  90. package/dist/elevenlabs-voice.js.map +1 -0
  91. package/dist/embeddings.cjs +8 -4
  92. package/dist/embeddings.cjs.map +1 -1
  93. package/dist/embeddings.d.cts.map +1 -1
  94. package/dist/embeddings.d.ts.map +1 -1
  95. package/dist/embeddings.js +9 -5
  96. package/dist/embeddings.js.map +1 -1
  97. package/dist/fal-audio.cjs +3 -3
  98. package/dist/fal-audio.cjs.map +1 -1
  99. package/dist/fal-audio.js +4 -4
  100. package/dist/fal-audio.js.map +1 -1
  101. package/dist/fal.cjs +28 -4
  102. package/dist/fal.cjs.map +1 -1
  103. package/dist/fal.d.cts +16 -1
  104. package/dist/fal.d.cts.map +1 -1
  105. package/dist/fal.d.ts +16 -1
  106. package/dist/fal.d.ts.map +1 -1
  107. package/dist/fal.js +28 -5
  108. package/dist/fal.js.map +1 -1
  109. package/dist/files.cjs +1071 -0
  110. package/dist/files.cjs.map +1 -0
  111. package/dist/files.d.cts +68 -0
  112. package/dist/files.d.cts.map +1 -0
  113. package/dist/files.d.ts +68 -0
  114. package/dist/files.d.ts.map +1 -0
  115. package/dist/files.js +1059 -0
  116. package/dist/files.js.map +1 -0
  117. package/dist/fine-tuning.cjs +1051 -0
  118. package/dist/fine-tuning.cjs.map +1 -0
  119. package/dist/fine-tuning.d.cts +96 -0
  120. package/dist/fine-tuning.d.cts.map +1 -0
  121. package/dist/fine-tuning.d.ts +96 -0
  122. package/dist/fine-tuning.d.ts.map +1 -0
  123. package/dist/fine-tuning.js +1044 -0
  124. package/dist/fine-tuning.js.map +1 -0
  125. package/dist/fixture-loader.cjs +0 -0
  126. package/dist/fixture-loader.cjs.map +1 -1
  127. package/dist/fixture-loader.d.cts +37 -5
  128. package/dist/fixture-loader.d.cts.map +1 -1
  129. package/dist/fixture-loader.d.ts +37 -5
  130. package/dist/fixture-loader.d.ts.map +1 -1
  131. package/dist/fixture-loader.js +0 -0
  132. package/dist/fixture-loader.js.map +1 -1
  133. package/dist/gemini-embeddings.cjs +2 -2
  134. package/dist/gemini-embeddings.cjs.map +1 -1
  135. package/dist/gemini-embeddings.js +3 -3
  136. package/dist/gemini-embeddings.js.map +1 -1
  137. package/dist/gemini-interactions.cjs +13 -7
  138. package/dist/gemini-interactions.cjs.map +1 -1
  139. package/dist/gemini-interactions.d.cts.map +1 -1
  140. package/dist/gemini-interactions.d.ts.map +1 -1
  141. package/dist/gemini-interactions.js +15 -9
  142. package/dist/gemini-interactions.js.map +1 -1
  143. package/dist/gemini.cjs +11 -7
  144. package/dist/gemini.cjs.map +1 -1
  145. package/dist/gemini.d.cts.map +1 -1
  146. package/dist/gemini.d.ts.map +1 -1
  147. package/dist/gemini.js +13 -9
  148. package/dist/gemini.js.map +1 -1
  149. package/dist/grok-video.cjs +14 -14
  150. package/dist/grok-video.cjs.map +1 -1
  151. package/dist/grok-video.js +15 -15
  152. package/dist/grok-video.js.map +1 -1
  153. package/dist/helpers.cjs +229 -7
  154. package/dist/helpers.cjs.map +1 -1
  155. package/dist/helpers.d.cts +38 -2
  156. package/dist/helpers.d.cts.map +1 -1
  157. package/dist/helpers.d.ts +38 -2
  158. package/dist/helpers.d.ts.map +1 -1
  159. package/dist/helpers.js +221 -8
  160. package/dist/helpers.js.map +1 -1
  161. package/dist/images.cjs +54 -123
  162. package/dist/images.cjs.map +1 -1
  163. package/dist/images.d.cts.map +1 -1
  164. package/dist/images.d.ts.map +1 -1
  165. package/dist/images.js +55 -124
  166. package/dist/images.js.map +1 -1
  167. package/dist/index.cjs +41 -8
  168. package/dist/index.d.cts +12 -7
  169. package/dist/index.d.ts +12 -7
  170. package/dist/index.js +14 -10
  171. package/dist/jest.cjs +14 -7
  172. package/dist/jest.cjs.map +1 -1
  173. package/dist/jest.d.cts +3 -2
  174. package/dist/jest.d.cts.map +1 -1
  175. package/dist/jest.d.ts +3 -2
  176. package/dist/jest.d.ts.map +1 -1
  177. package/dist/jest.js +14 -7
  178. package/dist/jest.js.map +1 -1
  179. package/dist/journal.cjs +41 -4
  180. package/dist/journal.cjs.map +1 -1
  181. package/dist/journal.d.cts +44 -3
  182. package/dist/journal.d.cts.map +1 -1
  183. package/dist/journal.d.ts +44 -3
  184. package/dist/journal.d.ts.map +1 -1
  185. package/dist/journal.js +41 -5
  186. package/dist/journal.js.map +1 -1
  187. package/dist/live-fixture.cjs +1192 -0
  188. package/dist/live-fixture.cjs.map +1 -0
  189. package/dist/live-fixture.js +1187 -0
  190. package/dist/live-fixture.js.map +1 -0
  191. package/dist/live-recorder.cjs +256 -0
  192. package/dist/live-recorder.cjs.map +1 -0
  193. package/dist/live-recorder.js +254 -0
  194. package/dist/live-recorder.js.map +1 -0
  195. package/dist/live-replay.cjs +286 -0
  196. package/dist/live-replay.cjs.map +1 -0
  197. package/dist/live-replay.js +285 -0
  198. package/dist/live-replay.js.map +1 -0
  199. package/dist/live-sanitize.cjs +196 -0
  200. package/dist/live-sanitize.cjs.map +1 -0
  201. package/dist/live-sanitize.js +196 -0
  202. package/dist/live-sanitize.js.map +1 -0
  203. package/dist/live-types.d.cts +71 -0
  204. package/dist/live-types.d.cts.map +1 -0
  205. package/dist/live-types.d.ts +71 -0
  206. package/dist/live-types.d.ts.map +1 -0
  207. package/dist/llmock.cjs +52 -41
  208. package/dist/llmock.cjs.map +1 -1
  209. package/dist/llmock.d.cts +16 -5
  210. package/dist/llmock.d.cts.map +1 -1
  211. package/dist/llmock.d.ts +16 -5
  212. package/dist/llmock.d.ts.map +1 -1
  213. package/dist/llmock.js +53 -42
  214. package/dist/llmock.js.map +1 -1
  215. package/dist/messages.cjs +11 -7
  216. package/dist/messages.cjs.map +1 -1
  217. package/dist/messages.d.cts.map +1 -1
  218. package/dist/messages.d.ts.map +1 -1
  219. package/dist/messages.js +13 -9
  220. package/dist/messages.js.map +1 -1
  221. package/dist/metrics.cjs +258 -5
  222. package/dist/metrics.cjs.map +1 -1
  223. package/dist/metrics.d.cts +3 -1
  224. package/dist/metrics.d.cts.map +1 -1
  225. package/dist/metrics.d.ts +3 -1
  226. package/dist/metrics.d.ts.map +1 -1
  227. package/dist/metrics.js +255 -6
  228. package/dist/metrics.js.map +1 -1
  229. package/dist/moderation.cjs +10 -1
  230. package/dist/moderation.cjs.map +1 -1
  231. package/dist/moderation.d.cts.map +1 -1
  232. package/dist/moderation.d.ts.map +1 -1
  233. package/dist/moderation.js +10 -1
  234. package/dist/moderation.js.map +1 -1
  235. package/dist/ollama.cjs +5 -5
  236. package/dist/ollama.cjs.map +1 -1
  237. package/dist/ollama.js +6 -6
  238. package/dist/ollama.js.map +1 -1
  239. package/dist/openrouter-video.cjs +5 -5
  240. package/dist/openrouter-video.cjs.map +1 -1
  241. package/dist/openrouter-video.js +6 -6
  242. package/dist/openrouter-video.js.map +1 -1
  243. package/dist/recorder.cjs +45 -9
  244. package/dist/recorder.cjs.map +1 -1
  245. package/dist/recorder.d.cts.map +1 -1
  246. package/dist/recorder.d.ts.map +1 -1
  247. package/dist/recorder.js +43 -11
  248. package/dist/recorder.js.map +1 -1
  249. package/dist/responses.cjs +4 -3
  250. package/dist/responses.cjs.map +1 -1
  251. package/dist/responses.js +5 -5
  252. package/dist/responses.js.map +1 -1
  253. package/dist/router.cjs +20 -33
  254. package/dist/router.cjs.map +1 -1
  255. package/dist/router.d.cts +4 -1
  256. package/dist/router.d.cts.map +1 -1
  257. package/dist/router.d.ts +4 -1
  258. package/dist/router.d.ts.map +1 -1
  259. package/dist/router.js +20 -34
  260. package/dist/router.js.map +1 -1
  261. package/dist/server.cjs +701 -625
  262. package/dist/server.cjs.map +1 -1
  263. package/dist/server.d.cts +1 -0
  264. package/dist/server.d.cts.map +1 -1
  265. package/dist/server.d.ts +1 -0
  266. package/dist/server.d.ts.map +1 -1
  267. package/dist/server.js +704 -628
  268. package/dist/server.js.map +1 -1
  269. package/dist/speech.cjs +2 -2
  270. package/dist/speech.cjs.map +1 -1
  271. package/dist/speech.js +3 -3
  272. package/dist/speech.js.map +1 -1
  273. package/dist/transcription.cjs +4 -4
  274. package/dist/transcription.cjs.map +1 -1
  275. package/dist/transcription.js +5 -5
  276. package/dist/transcription.js.map +1 -1
  277. package/dist/types.d.cts +119 -16
  278. package/dist/types.d.cts.map +1 -1
  279. package/dist/types.d.ts +119 -16
  280. package/dist/types.d.ts.map +1 -1
  281. package/dist/validate-cli.cjs +925 -0
  282. package/dist/validate-cli.cjs.map +1 -0
  283. package/dist/validate-cli.js +924 -0
  284. package/dist/validate-cli.js.map +1 -0
  285. package/dist/vector-types.d.cts.map +1 -1
  286. package/dist/vector-types.d.ts.map +1 -1
  287. package/dist/veo-video.cjs +13 -13
  288. package/dist/veo-video.cjs.map +1 -1
  289. package/dist/veo-video.js +14 -14
  290. package/dist/veo-video.js.map +1 -1
  291. package/dist/video.cjs +4 -4
  292. package/dist/video.cjs.map +1 -1
  293. package/dist/video.d.cts +1 -1
  294. package/dist/video.d.cts.map +1 -1
  295. package/dist/video.d.ts +1 -1
  296. package/dist/video.d.ts.map +1 -1
  297. package/dist/video.js +5 -5
  298. package/dist/video.js.map +1 -1
  299. package/dist/vitest.cjs +13 -6
  300. package/dist/vitest.cjs.map +1 -1
  301. package/dist/vitest.d.cts +2 -1
  302. package/dist/vitest.d.cts.map +1 -1
  303. package/dist/vitest.d.ts +2 -1
  304. package/dist/vitest.d.ts.map +1 -1
  305. package/dist/vitest.js +14 -7
  306. package/dist/vitest.js.map +1 -1
  307. package/dist/watcher.cjs +2 -1
  308. package/dist/watcher.cjs.map +1 -1
  309. package/dist/watcher.js +2 -1
  310. package/dist/watcher.js.map +1 -1
  311. package/dist/ws-framing.cjs +150 -35
  312. package/dist/ws-framing.cjs.map +1 -1
  313. package/dist/ws-framing.d.cts +18 -3
  314. package/dist/ws-framing.d.cts.map +1 -1
  315. package/dist/ws-framing.d.ts +18 -3
  316. package/dist/ws-framing.d.ts.map +1 -1
  317. package/dist/ws-framing.js +151 -36
  318. package/dist/ws-framing.js.map +1 -1
  319. package/dist/ws-gemini-live.cjs +2 -2
  320. package/dist/ws-gemini-live.d.ts +2 -2
  321. package/dist/ws-gemini-live.js +2 -2
  322. package/dist/ws-live.cjs +401 -0
  323. package/dist/ws-live.cjs.map +1 -0
  324. package/dist/ws-live.js +400 -0
  325. package/dist/ws-live.js.map +1 -0
  326. package/dist/ws-realtime.cjs +46 -2
  327. package/dist/ws-realtime.cjs.map +1 -1
  328. package/dist/ws-realtime.d.cts.map +1 -1
  329. package/dist/ws-realtime.d.ts +2 -2
  330. package/dist/ws-realtime.d.ts.map +1 -1
  331. package/dist/ws-realtime.js +46 -2
  332. package/dist/ws-realtime.js.map +1 -1
  333. package/dist/ws-responses.cjs +2 -2
  334. package/dist/ws-responses.js +2 -2
  335. package/dist/ws-upstream.cjs +118 -0
  336. package/dist/ws-upstream.cjs.map +1 -0
  337. package/dist/ws-upstream.js +115 -0
  338. package/dist/ws-upstream.js.map +1 -0
  339. package/fixtures/openai-live-client.json +390 -0
  340. package/fixtures/openai-live-managed.json +709 -0
  341. package/package.json +1 -1
@@ -0,0 +1,453 @@
1
+ import { flattenHeaders, generateId, isJsonObject } from "./helpers.js";
2
+ import { applyChaosAsync } from "./chaos.js";
3
+ import { buildTextResponse } from "./responses.js";
4
+ import { paginate, readMetadata } from "./fine-tuning.js";
5
+ import { FILES_MAX_BYTES, getStoredFileBytes, storeFile } from "./files.js";
6
+ import { randomBytes } from "node:crypto";
7
+
8
+ //#region src/batches.ts
9
+ /**
10
+ * The statuses a batch never leaves. Single source of truth: `advance()`
11
+ * refuses to move a terminal batch and the cancel handler rejects one, so the
12
+ * two cannot drift apart.
13
+ */
14
+ const TERMINAL_STATUSES = new Set([
15
+ "completed",
16
+ "failed",
17
+ "expired",
18
+ "cancelled"
19
+ ]);
20
+ function isTerminal(status) {
21
+ return TERMINAL_STATUSES.has(status);
22
+ }
23
+ /** Values accepted on `X-AIMock-Batch-Outcome`; where a batch lands on its 2nd retrieve. */
24
+ const OUTCOMES = new Set([
25
+ "completed",
26
+ "failed",
27
+ "expired",
28
+ "cancelled"
29
+ ]);
30
+ const OUTCOME_HEADER = "x-aimock-batch-outcome";
31
+ const VALID_ENDPOINTS = new Set([
32
+ "/v1/responses",
33
+ "/v1/chat/completions",
34
+ "/v1/embeddings",
35
+ "/v1/completions"
36
+ ]);
37
+ const batches = /* @__PURE__ */ new Map();
38
+ const batchPolls = /* @__PURE__ */ new Map();
39
+ const batchOutcomes = /* @__PURE__ */ new Map();
40
+ function clearBatchStore() {
41
+ batches.clear();
42
+ batchPolls.clear();
43
+ batchOutcomes.clear();
44
+ }
45
+ function journalBatches(journal, method, path, headers, status) {
46
+ journal.add({
47
+ method,
48
+ path,
49
+ headers,
50
+ body: null,
51
+ service: "batches",
52
+ response: {
53
+ status,
54
+ fixture: null
55
+ }
56
+ });
57
+ }
58
+ function writeJson(res, status, payload, setCorsHeaders) {
59
+ setCorsHeaders(res);
60
+ res.writeHead(status, { "Content-Type": "application/json" });
61
+ res.end(JSON.stringify(payload));
62
+ }
63
+ function invalid(message) {
64
+ return { error: {
65
+ message,
66
+ type: "invalid_request_error"
67
+ } };
68
+ }
69
+ /**
70
+ * Roll the chaos dice for one batches request. Returns true when a fault
71
+ * fired and the response is already written, so the caller returns early.
72
+ *
73
+ * CORS headers go on BEFORE the roll, because a fault writes the response
74
+ * itself and never reaches `writeJson`. `service: "batches"` on the journal
75
+ * context keeps faulted requests visible under `?service=batches`. Mirrors
76
+ * the `chaosHit` in `src/fine-tuning.ts`.
77
+ */
78
+ async function chaosHit(req, journal, defaults, method, path, res, setCorsHeaders) {
79
+ setCorsHeaders(res);
80
+ return await applyChaosAsync(res, null, defaults.chaos, req.headers, req.url, journal, {
81
+ method,
82
+ path,
83
+ headers: flattenHeaders(req.headers),
84
+ body: null,
85
+ service: "batches"
86
+ }, "internal", defaults.registry, defaults.logger);
87
+ }
88
+ function now() {
89
+ return Math.floor(Date.now() / 1e3);
90
+ }
91
+ /**
92
+ * The input file's requests, as `{ custom_id, model }` per JSONL line. Lines
93
+ * that are not JSON objects still count as requests (the real API reports
94
+ * them as errors by line; here they get a positional `custom_id`). An
95
+ * `input_file_id` that was never uploaded to this mock yields one synthesized
96
+ * request — see the module docblock.
97
+ */
98
+ function inputRequests(batch) {
99
+ const bytes = getStoredFileBytes(batch.input_file_id);
100
+ const requests = (bytes === void 0 ? [] : bytes.toString("utf8").split("\n").filter((l) => l.trim() !== "")).map((line, i) => {
101
+ let parsed;
102
+ try {
103
+ parsed = JSON.parse(line);
104
+ } catch {
105
+ parsed = void 0;
106
+ }
107
+ const obj = isJsonObject(parsed) ? parsed : {};
108
+ const body = isJsonObject(obj["body"]) ? obj["body"] : {};
109
+ return {
110
+ custom_id: typeof obj["custom_id"] === "string" ? obj["custom_id"] : `request-${i + 1}`,
111
+ model: typeof body["model"] === "string" ? body["model"] : "aimock"
112
+ };
113
+ });
114
+ return requests.length > 0 ? requests : [{
115
+ custom_id: "request-1",
116
+ model: "aimock"
117
+ }];
118
+ }
119
+ /** A minimal per-endpoint success body for one output line. */
120
+ function outputBody(endpoint, model) {
121
+ const created = now();
122
+ if (endpoint === "/v1/responses") return buildTextResponse("Mock batch completion", model);
123
+ if (endpoint === "/v1/embeddings") return {
124
+ object: "list",
125
+ model,
126
+ data: [{
127
+ object: "embedding",
128
+ index: 0,
129
+ embedding: [
130
+ 0,
131
+ 0,
132
+ 0
133
+ ]
134
+ }],
135
+ usage: {
136
+ prompt_tokens: 1,
137
+ total_tokens: 1
138
+ }
139
+ };
140
+ if (endpoint === "/v1/completions") return {
141
+ id: generateId("cmpl"),
142
+ object: "text_completion",
143
+ created,
144
+ model,
145
+ choices: [{
146
+ index: 0,
147
+ text: "Mock batch completion",
148
+ finish_reason: "stop"
149
+ }],
150
+ usage: {
151
+ prompt_tokens: 1,
152
+ completion_tokens: 1,
153
+ total_tokens: 2
154
+ }
155
+ };
156
+ return {
157
+ id: generateId("chatcmpl"),
158
+ object: "chat.completion",
159
+ created,
160
+ model,
161
+ choices: [{
162
+ index: 0,
163
+ message: {
164
+ role: "assistant",
165
+ content: "Mock batch completion"
166
+ },
167
+ finish_reason: "stop"
168
+ }],
169
+ usage: {
170
+ prompt_tokens: 1,
171
+ completion_tokens: 1,
172
+ total_tokens: 2
173
+ }
174
+ };
175
+ }
176
+ /** `batch_req_…` / `req_…` — underscore-joined like the real ids, unlike `generateId`. */
177
+ function underscoreId(prefix) {
178
+ return `${prefix}_${randomBytes(12).toString("base64url")}`;
179
+ }
180
+ /**
181
+ * Serialise one JSONL line per request, stopping the moment the running byte
182
+ * count passes the Files store's per-file cap. Every output line is a fixed
183
+ * ~430-byte canned body regardless of how small the input line was (a 3-byte
184
+ * `{}` line amplifies ~143x), so an input file that is itself under the
185
+ * upload cap can describe an output far over it; building the lines eagerly
186
+ * would hold that whole over-cap buffer before anything could refuse it.
187
+ * Returns `undefined` when the cap is exceeded, with at most `FILES_MAX_BYTES`
188
+ * ever materialised.
189
+ */
190
+ function jsonlUnderCap(items, line) {
191
+ const chunks = [];
192
+ let bytes = 0;
193
+ for (const item of items) {
194
+ const chunk = Buffer.from(JSON.stringify(line(item)) + "\n", "utf8");
195
+ bytes += chunk.length;
196
+ if (bytes > FILES_MAX_BYTES) return;
197
+ chunks.push(chunk);
198
+ }
199
+ return Buffer.concat(chunks, bytes);
200
+ }
201
+ /**
202
+ * The batch could not mint its result file under {@link FILES_MAX_BYTES}: it
203
+ * fails with one `errors` entry naming the cap. `BatchError.code` is an open
204
+ * `string` in the SDK types and the real API has no equivalent condition, so
205
+ * the code value is UNVERIFIED against a live capture.
206
+ */
207
+ function failOverCap(batch, total, kind) {
208
+ batch.status = "failed";
209
+ batch.errors = {
210
+ object: "list",
211
+ data: [{
212
+ code: "output_file_too_large",
213
+ message: `Batch ${kind} file would exceed the ${FILES_MAX_BYTES} byte per-file cap`,
214
+ line: null,
215
+ param: null
216
+ }]
217
+ };
218
+ batch.request_counts = {
219
+ total,
220
+ completed: 0,
221
+ failed: total
222
+ };
223
+ batch.failed_at = now();
224
+ }
225
+ function finish(batch, outcome) {
226
+ const requests = inputRequests(batch);
227
+ const total = requests.length;
228
+ batch.status = outcome;
229
+ if (outcome === "completed") {
230
+ const content = jsonlUnderCap(requests, (r) => ({
231
+ id: underscoreId("batch_req"),
232
+ custom_id: r.custom_id,
233
+ response: {
234
+ status_code: 200,
235
+ request_id: underscoreId("req"),
236
+ body: outputBody(batch.endpoint, r.model)
237
+ },
238
+ error: null
239
+ }));
240
+ if (content === void 0) {
241
+ failOverCap(batch, total, "output");
242
+ return;
243
+ }
244
+ batch.output_file_id = storeFile({
245
+ purpose: "batch_output",
246
+ filename: `${batch.id}_output.jsonl`,
247
+ content
248
+ }).id;
249
+ batch.request_counts = {
250
+ total,
251
+ completed: total,
252
+ failed: 0
253
+ };
254
+ batch.completed_at = now();
255
+ } else if (outcome === "failed") {
256
+ const message = "Mock batch failure (X-AIMock-Batch-Outcome: failed)";
257
+ const content = jsonlUnderCap(requests, (r) => ({
258
+ id: underscoreId("batch_req"),
259
+ custom_id: r.custom_id,
260
+ response: null,
261
+ error: {
262
+ code: "mock_failure",
263
+ message
264
+ }
265
+ }));
266
+ if (content === void 0) {
267
+ failOverCap(batch, total, "error");
268
+ return;
269
+ }
270
+ batch.error_file_id = storeFile({
271
+ purpose: "batch_output",
272
+ filename: `${batch.id}_error.jsonl`,
273
+ content
274
+ }).id;
275
+ batch.errors = {
276
+ object: "list",
277
+ data: [{
278
+ code: "mock_failure",
279
+ message: `${message}: see error_file_id`,
280
+ line: null,
281
+ param: null
282
+ }]
283
+ };
284
+ batch.request_counts = {
285
+ total,
286
+ completed: 0,
287
+ failed: total
288
+ };
289
+ batch.failed_at = now();
290
+ } else if (outcome === "expired") {
291
+ batch.request_counts = {
292
+ total,
293
+ completed: 0,
294
+ failed: 0
295
+ };
296
+ batch.expired_at = now();
297
+ } else {
298
+ batch.request_counts = {
299
+ total,
300
+ completed: 0,
301
+ failed: 0
302
+ };
303
+ batch.cancelled_at = now();
304
+ }
305
+ }
306
+ function advance(batch) {
307
+ if (isTerminal(batch.status)) return batch;
308
+ if (batch.status === "cancelling") {
309
+ finish(batch, "cancelled");
310
+ return batch;
311
+ }
312
+ const polls = (batchPolls.get(batch.id) ?? 0) + 1;
313
+ batchPolls.set(batch.id, polls);
314
+ if (polls === 1) {
315
+ batch.status = "in_progress";
316
+ batch.in_progress_at = now();
317
+ batch.request_counts = {
318
+ total: inputRequests(batch).length,
319
+ completed: 0,
320
+ failed: 0
321
+ };
322
+ } else finish(batch, batchOutcomes.get(batch.id) ?? "completed");
323
+ return batch;
324
+ }
325
+ async function handleBatchesCreate(req, res, raw, journal, defaults, setCorsHeaders) {
326
+ const path = req.url ?? "/v1/batches";
327
+ const method = req.method ?? "POST";
328
+ if (await chaosHit(req, journal, defaults, method, path, res, setCorsHeaders)) return;
329
+ let body;
330
+ try {
331
+ body = JSON.parse(raw);
332
+ } catch (err) {
333
+ const detail = err instanceof Error ? err.message : "unknown";
334
+ journalBatches(journal, method, path, flattenHeaders(req.headers), 400);
335
+ writeJson(res, 400, invalid(`Malformed JSON: ${detail}`), setCorsHeaders);
336
+ return;
337
+ }
338
+ if (!isJsonObject(body)) {
339
+ journalBatches(journal, method, path, flattenHeaders(req.headers), 400);
340
+ writeJson(res, 400, invalid("Request body must be a JSON object"), setCorsHeaders);
341
+ return;
342
+ }
343
+ const inputFileId = body["input_file_id"];
344
+ const endpoint = body["endpoint"];
345
+ const window = body["completion_window"];
346
+ if (typeof inputFileId !== "string" || inputFileId.length === 0) {
347
+ journalBatches(journal, method, path, flattenHeaders(req.headers), 400);
348
+ writeJson(res, 400, invalid("Invalid parameter: 'input_file_id' must be a non-empty string"), setCorsHeaders);
349
+ return;
350
+ }
351
+ if (typeof endpoint !== "string" || !VALID_ENDPOINTS.has(endpoint)) {
352
+ journalBatches(journal, method, path, flattenHeaders(req.headers), 400);
353
+ writeJson(res, 400, invalid(`Invalid endpoint '${String(endpoint)}'. Expected one of: ${[...VALID_ENDPOINTS].join(", ")}`), setCorsHeaders);
354
+ return;
355
+ }
356
+ if (window !== "24h") {
357
+ journalBatches(journal, method, path, flattenHeaders(req.headers), 400);
358
+ writeJson(res, 400, invalid("Invalid parameter: 'completion_window' must be '24h'"), setCorsHeaders);
359
+ return;
360
+ }
361
+ const metadata = readMetadata(body["metadata"]);
362
+ if (!metadata.ok) {
363
+ journalBatches(journal, method, path, flattenHeaders(req.headers), 400);
364
+ writeJson(res, 400, invalid(metadata.message), setCorsHeaders);
365
+ return;
366
+ }
367
+ const outcomeHeader = req.headers[OUTCOME_HEADER];
368
+ const outcomeRaw = (Array.isArray(outcomeHeader) ? outcomeHeader[0] : outcomeHeader)?.trim().toLowerCase();
369
+ if (outcomeRaw !== void 0 && !OUTCOMES.has(outcomeRaw)) {
370
+ journalBatches(journal, method, path, flattenHeaders(req.headers), 400);
371
+ writeJson(res, 400, invalid(`Invalid X-AIMock-Batch-Outcome '${outcomeRaw}'. Expected one of: ${[...OUTCOMES].join(", ")}`), setCorsHeaders);
372
+ return;
373
+ }
374
+ const id = generateId("batch");
375
+ const batch = {
376
+ id,
377
+ object: "batch",
378
+ endpoint,
379
+ input_file_id: inputFileId,
380
+ completion_window: "24h",
381
+ status: "validating",
382
+ created_at: Math.floor(Date.now() / 1e3),
383
+ request_counts: {
384
+ total: 0,
385
+ completed: 0,
386
+ failed: 0
387
+ }
388
+ };
389
+ if (metadata.value !== null) batch.metadata = metadata.value;
390
+ batches.set(id, batch);
391
+ batchPolls.set(id, 0);
392
+ if (outcomeRaw !== void 0) batchOutcomes.set(id, outcomeRaw);
393
+ defaults.logger.debug(`Batches mock: created ${id}`);
394
+ journalBatches(journal, method, path, flattenHeaders(req.headers), 200);
395
+ writeJson(res, 200, batch, setCorsHeaders);
396
+ }
397
+ async function handleBatchesList(req, res, journal, defaults, setCorsHeaders) {
398
+ const path = req.url ?? "/v1/batches";
399
+ const method = req.method ?? "GET";
400
+ if (await chaosHit(req, journal, defaults, method, path, res, setCorsHeaders)) return;
401
+ const result = paginate([...batches.values()].reverse(), req.url);
402
+ if (!result.ok) {
403
+ journalBatches(journal, method, path, flattenHeaders(req.headers), 400);
404
+ writeJson(res, 400, invalid(result.message), setCorsHeaders);
405
+ return;
406
+ }
407
+ const page = result.page.data;
408
+ journalBatches(journal, method, path, flattenHeaders(req.headers), 200);
409
+ writeJson(res, 200, {
410
+ ...result.page,
411
+ first_id: page[0]?.id ?? null,
412
+ last_id: page[page.length - 1]?.id ?? null
413
+ }, setCorsHeaders);
414
+ }
415
+ async function handleBatchesRetrieve(req, res, batchId, journal, defaults, setCorsHeaders) {
416
+ const path = req.url ?? `/v1/batches/${batchId}`;
417
+ const method = req.method ?? "GET";
418
+ if (await chaosHit(req, journal, defaults, method, path, res, setCorsHeaders)) return;
419
+ const batch = batches.get(batchId);
420
+ if (!batch) {
421
+ journalBatches(journal, method, path, flattenHeaders(req.headers), 404);
422
+ writeJson(res, 404, invalid(`No such batch: ${batchId}`), setCorsHeaders);
423
+ return;
424
+ }
425
+ journalBatches(journal, method, path, flattenHeaders(req.headers), 200);
426
+ writeJson(res, 200, advance(batch), setCorsHeaders);
427
+ }
428
+ async function handleBatchesCancel(req, res, batchId, journal, defaults, setCorsHeaders) {
429
+ const path = req.url ?? `/v1/batches/${batchId}/cancel`;
430
+ const method = req.method ?? "POST";
431
+ if (await chaosHit(req, journal, defaults, method, path, res, setCorsHeaders)) return;
432
+ const batch = batches.get(batchId);
433
+ if (!batch) {
434
+ journalBatches(journal, method, path, flattenHeaders(req.headers), 404);
435
+ writeJson(res, 404, invalid(`No such batch: ${batchId}`), setCorsHeaders);
436
+ return;
437
+ }
438
+ if (isTerminal(batch.status)) {
439
+ journalBatches(journal, method, path, flattenHeaders(req.headers), 400);
440
+ writeJson(res, 400, invalid(`Cannot cancel a batch with status ${batch.status}.`), setCorsHeaders);
441
+ return;
442
+ }
443
+ if (batch.status !== "cancelling") {
444
+ batch.status = "cancelling";
445
+ batch.cancelling_at = now();
446
+ }
447
+ journalBatches(journal, method, path, flattenHeaders(req.headers), 200);
448
+ writeJson(res, 200, batch, setCorsHeaders);
449
+ }
450
+
451
+ //#endregion
452
+ export { clearBatchStore, handleBatchesCancel, handleBatchesCreate, handleBatchesList, handleBatchesRetrieve };
453
+ //# sourceMappingURL=batches.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"batches.js","names":[],"sources":["../src/batches.ts"],"sourcesContent":["/**\n * OpenAI Batches API mock.\n *\n * Covers `POST /v1/batches`, `GET /v1/batches`, `GET /v1/batches/{id}` and\n * `POST /v1/batches/{id}/cancel` with a deterministic poll progression so\n * batch flows can be tested without waiting 24h:\n *\n * validating → in_progress → completed | failed | expired | cancelled\n *\n * The first retrieve after create advances to `in_progress`, the second (and\n * later) to the batch's outcome. Terminal batches are stable. On `completed`\n * the mock registers a REAL file in the Files store (`output_file_id`, purpose\n * `batch_output`, filename `<batch-id>_output.jsonl`) holding one JSONL line\n * per input request, so `GET /v1/files/{id}/content` serves it back and the\n * upload → create → poll → download flow closes. When `input_file_id` names a\n * file in the store, its JSONL lines drive the `custom_id`s and\n * `request_counts.total`; when it does not (the id was never uploaded here),\n * the mock synthesizes ONE request (`custom_id: \"request-1\"`, `total: 1`) so\n * the output file is still downloadable.\n *\n * Outcome control: the real API only ever completes a healthy batch, so a\n * suite that has to exercise its failure handling needs a lever. Send\n * `X-AIMock-Batch-Outcome: completed | failed | expired | cancelled` on\n * `POST /v1/batches` (default `completed`, unknown values 400). `failed`\n * populates a one-entry `errors` list and a real `error_file_id` (one JSONL\n * error line per request) and stamps `failed_at`; `expired` stamps `expired_at`; `cancelled`\n * lands the batch on `cancelled` without a cancel call. Same `X-AIMock-*`\n * family as `X-AIMock-Strict` and the chaos headers.\n *\n * `cancel` on a non-terminal batch answers `cancelling` (stamping\n * `cancelling_at`); the next retrieve shows `cancelled` with `cancelled_at`.\n * Cancelling a terminal batch is 400 `Cannot cancel a batch with status\n * <status>.` (the SDK's `Batch.status` union, openai@4.104.0\n * `resources/batches.d.ts`, is the source for the status set and the `*_at`\n * stamps; the exact 400 message text is UNVERIFIED against the live API).\n *\n * Output-line shape (`{id: \"batch_req_…\", custom_id, response: {status_code,\n * request_id, body}, error}` / error lines with `response: null` and\n * `error: {code, message}`) follows the OpenAI Batch guide's documented\n * result-file format; neither the vendored SDK nor `openai/openai-openapi`\n * types the file's lines, so that shape is UNVERIFIED against a live capture.\n *\n * State is in-memory per process and cleared by the full reset path.\n * Every branch journals with `service: \"batches\"` and runs through the\n * chaos gate so fault-injection suites can target the batch surface.\n */\n\nimport type * as http from \"node:http\";\nimport { randomBytes } from \"node:crypto\";\nimport { flattenHeaders, generateId, isJsonObject } from \"./helpers.js\";\nimport { applyChaosAsync, type ChaosAsyncOutcome } from \"./chaos.js\";\nimport { paginate, readMetadata } from \"./fine-tuning.js\";\nimport { FILES_MAX_BYTES, getStoredFileBytes, storeFile } from \"./files.js\";\nimport { buildTextResponse } from \"./responses.js\";\nimport type { ChaosDefaults } from \"./types.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\nimport type { MetricsRegistry } from \"./metrics.js\";\n\nexport interface BatchObject {\n id: string;\n object: \"batch\";\n endpoint: string;\n input_file_id: string;\n completion_window: string;\n status: BatchStatus;\n created_at: number;\n metadata?: Record<string, string>;\n in_progress_at?: number;\n completed_at?: number;\n failed_at?: number;\n expired_at?: number;\n cancelling_at?: number;\n cancelled_at?: number;\n output_file_id?: string;\n error_file_id?: string;\n errors?: { object: \"list\"; data: BatchError[] };\n request_counts?: { total: number; completed: number; failed: number };\n}\n\n/** `Batch.status` minus `finalizing`, which this mock never lingers in. */\nexport type BatchStatus =\n | \"validating\"\n | \"in_progress\"\n | \"completed\"\n | \"failed\"\n | \"expired\"\n | \"cancelling\"\n | \"cancelled\";\n\n/** `BatchError` from the SDK (`openai@4.104.0` `resources/batches.d.ts`). */\nexport interface BatchError {\n code?: string;\n line?: number | null;\n message?: string;\n param?: string | null;\n}\n\n/**\n * The statuses a batch never leaves. Single source of truth: `advance()`\n * refuses to move a terminal batch and the cancel handler rejects one, so the\n * two cannot drift apart.\n */\nconst TERMINAL_STATUSES = new Set<BatchStatus>([\"completed\", \"failed\", \"expired\", \"cancelled\"]);\n\nfunction isTerminal(status: BatchStatus): boolean {\n return TERMINAL_STATUSES.has(status);\n}\n\n/** Values accepted on `X-AIMock-Batch-Outcome`; where a batch lands on its 2nd retrieve. */\nconst OUTCOMES = new Set([\"completed\", \"failed\", \"expired\", \"cancelled\"]);\ntype BatchOutcome = \"completed\" | \"failed\" | \"expired\" | \"cancelled\";\nconst OUTCOME_HEADER = \"x-aimock-batch-outcome\";\n\n// The `BatchCreateParams.endpoint` union in the vendored `openai` SDK 4.104.0\n// (`resources/batches.d.ts`): `/v1/responses`, `/v1/chat/completions`,\n// `/v1/embeddings`, `/v1/completions`.\nconst VALID_ENDPOINTS = new Set([\n \"/v1/responses\",\n \"/v1/chat/completions\",\n \"/v1/embeddings\",\n \"/v1/completions\",\n]);\n\nconst batches = new Map<string, BatchObject>();\nconst batchPolls = new Map<string, number>();\nconst batchOutcomes = new Map<string, BatchOutcome>();\n\nexport function clearBatchStore(): void {\n batches.clear();\n batchPolls.clear();\n batchOutcomes.clear();\n}\n\nfunction journalBatches(\n journal: Journal,\n method: string,\n path: string,\n headers: Record<string, string>,\n status: number,\n): void {\n journal.add({\n method,\n path,\n headers,\n body: null,\n service: \"batches\",\n response: { status, fixture: null },\n });\n}\n\nfunction writeJson(\n res: http.ServerResponse,\n status: number,\n payload: unknown,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n res.writeHead(status, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(payload));\n}\n\nfunction invalid(message: string): { error: { message: string; type: string } } {\n return { error: { message, type: \"invalid_request_error\" } };\n}\n\ntype Defaults = { logger: Logger; chaos?: ChaosDefaults; registry?: MetricsRegistry };\n\n/**\n * Roll the chaos dice for one batches request. Returns true when a fault\n * fired and the response is already written, so the caller returns early.\n *\n * CORS headers go on BEFORE the roll, because a fault writes the response\n * itself and never reaches `writeJson`. `service: \"batches\"` on the journal\n * context keeps faulted requests visible under `?service=batches`. Mirrors\n * the `chaosHit` in `src/fine-tuning.ts`.\n */\nasync function chaosHit(\n req: http.IncomingMessage,\n journal: Journal,\n defaults: Defaults,\n method: string,\n path: string,\n res: http.ServerResponse,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<ChaosAsyncOutcome> {\n setCorsHeaders(res);\n return await applyChaosAsync(\n res,\n null,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n {\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"batches\",\n },\n \"internal\",\n defaults.registry,\n defaults.logger,\n );\n}\n\nfunction now(): number {\n return Math.floor(Date.now() / 1000);\n}\n\n/**\n * The input file's requests, as `{ custom_id, model }` per JSONL line. Lines\n * that are not JSON objects still count as requests (the real API reports\n * them as errors by line; here they get a positional `custom_id`). An\n * `input_file_id` that was never uploaded to this mock yields one synthesized\n * request — see the module docblock.\n */\nfunction inputRequests(batch: BatchObject): { custom_id: string; model: string }[] {\n const bytes = getStoredFileBytes(batch.input_file_id);\n const lines =\n bytes === undefined\n ? []\n : bytes\n .toString(\"utf8\")\n .split(\"\\n\")\n .filter((l) => l.trim() !== \"\");\n const requests = lines.map((line, i) => {\n let parsed: unknown;\n try {\n parsed = JSON.parse(line) as unknown;\n } catch {\n parsed = undefined;\n }\n const obj = isJsonObject(parsed) ? parsed : {};\n const body = isJsonObject(obj[\"body\"]) ? obj[\"body\"] : {};\n return {\n custom_id: typeof obj[\"custom_id\"] === \"string\" ? obj[\"custom_id\"] : `request-${i + 1}`,\n model: typeof body[\"model\"] === \"string\" ? body[\"model\"] : \"aimock\",\n };\n });\n return requests.length > 0 ? requests : [{ custom_id: \"request-1\", model: \"aimock\" }];\n}\n\n/** A minimal per-endpoint success body for one output line. */\nfunction outputBody(endpoint: string, model: string): Record<string, unknown> {\n const created = now();\n if (endpoint === \"/v1/responses\") {\n // The same `Response` envelope the live `/v1/responses` mock writes\n // (`object: \"response\"`, `status`, `output[]` message with `output_text`,\n // `usage`), per openai@4.104.0 `resources/responses/responses.d.ts`.\n return buildTextResponse(\"Mock batch completion\", model) as Record<string, unknown>;\n }\n if (endpoint === \"/v1/embeddings\") {\n return {\n object: \"list\",\n model,\n data: [{ object: \"embedding\", index: 0, embedding: [0, 0, 0] }],\n usage: { prompt_tokens: 1, total_tokens: 1 },\n };\n }\n if (endpoint === \"/v1/completions\") {\n return {\n id: generateId(\"cmpl\"),\n object: \"text_completion\",\n created,\n model,\n choices: [{ index: 0, text: \"Mock batch completion\", finish_reason: \"stop\" }],\n usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },\n };\n }\n return {\n id: generateId(\"chatcmpl\"),\n object: \"chat.completion\",\n created,\n model,\n choices: [\n {\n index: 0,\n message: { role: \"assistant\", content: \"Mock batch completion\" },\n finish_reason: \"stop\",\n },\n ],\n usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },\n };\n}\n\n/** `batch_req_…` / `req_…` — underscore-joined like the real ids, unlike `generateId`. */\nfunction underscoreId(prefix: string): string {\n return `${prefix}_${randomBytes(12).toString(\"base64url\")}`;\n}\n\n/**\n * Serialise one JSONL line per request, stopping the moment the running byte\n * count passes the Files store's per-file cap. Every output line is a fixed\n * ~430-byte canned body regardless of how small the input line was (a 3-byte\n * `{}` line amplifies ~143x), so an input file that is itself under the\n * upload cap can describe an output far over it; building the lines eagerly\n * would hold that whole over-cap buffer before anything could refuse it.\n * Returns `undefined` when the cap is exceeded, with at most `FILES_MAX_BYTES`\n * ever materialised.\n */\nfunction jsonlUnderCap<T>(items: T[], line: (item: T) => unknown): Buffer | undefined {\n const chunks: Buffer[] = [];\n let bytes = 0;\n for (const item of items) {\n const chunk = Buffer.from(JSON.stringify(line(item)) + \"\\n\", \"utf8\");\n bytes += chunk.length;\n if (bytes > FILES_MAX_BYTES) {\n return undefined;\n }\n chunks.push(chunk);\n }\n return Buffer.concat(chunks, bytes);\n}\n\n/**\n * The batch could not mint its result file under {@link FILES_MAX_BYTES}: it\n * fails with one `errors` entry naming the cap. `BatchError.code` is an open\n * `string` in the SDK types and the real API has no equivalent condition, so\n * the code value is UNVERIFIED against a live capture.\n */\nfunction failOverCap(batch: BatchObject, total: number, kind: \"output\" | \"error\"): void {\n batch.status = \"failed\";\n batch.errors = {\n object: \"list\",\n data: [\n {\n code: \"output_file_too_large\",\n message: `Batch ${kind} file would exceed the ${FILES_MAX_BYTES} byte per-file cap`,\n line: null,\n param: null,\n },\n ],\n };\n batch.request_counts = { total, completed: 0, failed: total };\n batch.failed_at = now();\n}\n\nfunction finish(batch: BatchObject, outcome: BatchOutcome): void {\n const requests = inputRequests(batch);\n const total = requests.length;\n batch.status = outcome;\n if (outcome === \"completed\") {\n const content = jsonlUnderCap(requests, (r) => ({\n id: underscoreId(\"batch_req\"),\n custom_id: r.custom_id,\n response: {\n status_code: 200,\n request_id: underscoreId(\"req\"),\n body: outputBody(batch.endpoint, r.model),\n },\n error: null,\n }));\n if (content === undefined) {\n failOverCap(batch, total, \"output\");\n return;\n }\n batch.output_file_id = storeFile({\n purpose: \"batch_output\",\n filename: `${batch.id}_output.jsonl`,\n content,\n }).id;\n batch.request_counts = { total, completed: total, failed: 0 };\n batch.completed_at = now();\n } else if (outcome === \"failed\") {\n const message = \"Mock batch failure (X-AIMock-Batch-Outcome: failed)\";\n const content = jsonlUnderCap(requests, (r) => ({\n id: underscoreId(\"batch_req\"),\n custom_id: r.custom_id,\n response: null,\n error: { code: \"mock_failure\", message },\n }));\n if (content === undefined) {\n failOverCap(batch, total, \"error\");\n return;\n }\n batch.error_file_id = storeFile({\n purpose: \"batch_output\",\n filename: `${batch.id}_error.jsonl`,\n content,\n }).id;\n // One file-level entry, not one per request: `errors` rides inline on\n // every retrieve and every list page, so a per-line list scales the batch\n // object with the input (2000 lines -> ~225 KB per GET). The per-request\n // detail lives in the error file. (The live API's `Batch.Errors` is typed\n // as a plain `BatchError[]` in openai@4.104.0 `resources/batches.d.ts`;\n // that it stays small is UNVERIFIED there but matches its purpose of\n // reporting input-file validation errors, not per-request outcomes.)\n batch.errors = {\n object: \"list\",\n data: [\n { code: \"mock_failure\", message: `${message}: see error_file_id`, line: null, param: null },\n ],\n };\n batch.request_counts = { total, completed: 0, failed: total };\n batch.failed_at = now();\n } else if (outcome === \"expired\") {\n batch.request_counts = { total, completed: 0, failed: 0 };\n batch.expired_at = now();\n } else {\n batch.request_counts = { total, completed: 0, failed: 0 };\n batch.cancelled_at = now();\n }\n}\n\nfunction advance(batch: BatchObject): BatchObject {\n if (isTerminal(batch.status)) {\n return batch;\n }\n if (batch.status === \"cancelling\") {\n // Same arm the `X-AIMock-Batch-Outcome: cancelled` path lands on, so a\n // route-cancelled batch reports the same `request_counts`.\n finish(batch, \"cancelled\");\n return batch;\n }\n const polls = (batchPolls.get(batch.id) ?? 0) + 1;\n batchPolls.set(batch.id, polls);\n if (polls === 1) {\n batch.status = \"in_progress\";\n batch.in_progress_at = now();\n // `total` is known once the input has been read; the terminal arms in\n // `finish()` fill in `completed` / `failed`. (Whether the live API stamps\n // `total` exactly at `in_progress` is UNVERIFIED: openai@4.104.0\n // `resources/batches.d.ts` types `BatchRequestCounts` but not its timing.)\n batch.request_counts = { total: inputRequests(batch).length, completed: 0, failed: 0 };\n } else {\n finish(batch, batchOutcomes.get(batch.id) ?? \"completed\");\n }\n return batch;\n}\n\nexport async function handleBatchesCreate(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n journal: Journal,\n defaults: Defaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const path = req.url ?? \"/v1/batches\";\n const method = req.method ?? \"POST\";\n if (await chaosHit(req, journal, defaults, method, path, res, setCorsHeaders)) return;\n\n let body: unknown;\n try {\n body = JSON.parse(raw) as unknown;\n } catch (err) {\n const detail = err instanceof Error ? err.message : \"unknown\";\n journalBatches(journal, method, path, flattenHeaders(req.headers), 400);\n writeJson(res, 400, invalid(`Malformed JSON: ${detail}`), setCorsHeaders);\n return;\n }\n if (!isJsonObject(body)) {\n journalBatches(journal, method, path, flattenHeaders(req.headers), 400);\n writeJson(res, 400, invalid(\"Request body must be a JSON object\"), setCorsHeaders);\n return;\n }\n const inputFileId = body[\"input_file_id\"];\n const endpoint = body[\"endpoint\"];\n const window = body[\"completion_window\"];\n if (typeof inputFileId !== \"string\" || inputFileId.length === 0) {\n journalBatches(journal, method, path, flattenHeaders(req.headers), 400);\n writeJson(\n res,\n 400,\n invalid(\"Invalid parameter: 'input_file_id' must be a non-empty string\"),\n setCorsHeaders,\n );\n return;\n }\n if (typeof endpoint !== \"string\" || !VALID_ENDPOINTS.has(endpoint)) {\n journalBatches(journal, method, path, flattenHeaders(req.headers), 400);\n writeJson(\n res,\n 400,\n invalid(\n `Invalid endpoint '${String(endpoint)}'. Expected one of: ${[...VALID_ENDPOINTS].join(\", \")}`,\n ),\n setCorsHeaders,\n );\n return;\n }\n if (window !== \"24h\") {\n journalBatches(journal, method, path, flattenHeaders(req.headers), 400);\n writeJson(\n res,\n 400,\n invalid(\"Invalid parameter: 'completion_window' must be '24h'\"),\n setCorsHeaders,\n );\n return;\n }\n // Same shared `Metadata` schema the fine-tuning mock validates and echoes.\n const metadata = readMetadata(body[\"metadata\"]);\n if (!metadata.ok) {\n journalBatches(journal, method, path, flattenHeaders(req.headers), 400);\n writeJson(res, 400, invalid(metadata.message), setCorsHeaders);\n return;\n }\n\n const outcomeHeader = req.headers[OUTCOME_HEADER];\n const outcomeRaw = (Array.isArray(outcomeHeader) ? outcomeHeader[0] : outcomeHeader)\n ?.trim()\n .toLowerCase();\n if (outcomeRaw !== undefined && !OUTCOMES.has(outcomeRaw)) {\n journalBatches(journal, method, path, flattenHeaders(req.headers), 400);\n writeJson(\n res,\n 400,\n invalid(\n `Invalid X-AIMock-Batch-Outcome '${outcomeRaw}'. Expected one of: ${[...OUTCOMES].join(\", \")}`,\n ),\n setCorsHeaders,\n );\n return;\n }\n\n const id = generateId(\"batch\");\n const batch: BatchObject = {\n id,\n object: \"batch\",\n endpoint,\n input_file_id: inputFileId,\n completion_window: \"24h\",\n status: \"validating\",\n created_at: Math.floor(Date.now() / 1000),\n request_counts: { total: 0, completed: 0, failed: 0 },\n };\n if (metadata.value !== null) batch.metadata = metadata.value;\n batches.set(id, batch);\n batchPolls.set(id, 0);\n if (outcomeRaw !== undefined) batchOutcomes.set(id, outcomeRaw as BatchOutcome);\n defaults.logger.debug(`Batches mock: created ${id}`);\n journalBatches(journal, method, path, flattenHeaders(req.headers), 200);\n writeJson(res, 200, batch, setCorsHeaders);\n}\n\nexport async function handleBatchesList(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n journal: Journal,\n defaults: Defaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const path = req.url ?? \"/v1/batches\";\n const method = req.method ?? \"GET\";\n if (await chaosHit(req, journal, defaults, method, path, res, setCorsHeaders)) return;\n // Newest-first, mirroring `handleFineTuningList`: `paginate()` documents a\n // newest-first input, and reversing insertion order is the stable total\n // order a cursor walk needs (`created_at` has one-second resolution, so a\n // sort on it cannot order the ties that are the normal case).\n const data = [...batches.values()].reverse();\n // `after`/`limit` cursor paging shared with the fine-tuning mock; the real\n // list response also carries `first_id`/`last_id` (null on an empty page).\n const result = paginate(data, req.url);\n if (!result.ok) {\n journalBatches(journal, method, path, flattenHeaders(req.headers), 400);\n writeJson(res, 400, invalid(result.message), setCorsHeaders);\n return;\n }\n const page = result.page.data;\n journalBatches(journal, method, path, flattenHeaders(req.headers), 200);\n writeJson(\n res,\n 200,\n {\n ...result.page,\n first_id: page[0]?.id ?? null,\n last_id: page[page.length - 1]?.id ?? null,\n },\n setCorsHeaders,\n );\n}\n\nexport async function handleBatchesRetrieve(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n batchId: string,\n journal: Journal,\n defaults: Defaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const path = req.url ?? `/v1/batches/${batchId}`;\n const method = req.method ?? \"GET\";\n if (await chaosHit(req, journal, defaults, method, path, res, setCorsHeaders)) return;\n const batch = batches.get(batchId);\n if (!batch) {\n journalBatches(journal, method, path, flattenHeaders(req.headers), 404);\n writeJson(res, 404, invalid(`No such batch: ${batchId}`), setCorsHeaders);\n return;\n }\n journalBatches(journal, method, path, flattenHeaders(req.headers), 200);\n writeJson(res, 200, advance(batch), setCorsHeaders);\n}\n\nexport async function handleBatchesCancel(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n batchId: string,\n journal: Journal,\n defaults: Defaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const path = req.url ?? `/v1/batches/${batchId}/cancel`;\n const method = req.method ?? \"POST\";\n if (await chaosHit(req, journal, defaults, method, path, res, setCorsHeaders)) return;\n const batch = batches.get(batchId);\n if (!batch) {\n journalBatches(journal, method, path, flattenHeaders(req.headers), 404);\n writeJson(res, 404, invalid(`No such batch: ${batchId}`), setCorsHeaders);\n return;\n }\n if (isTerminal(batch.status)) {\n journalBatches(journal, method, path, flattenHeaders(req.headers), 400);\n writeJson(\n res,\n 400,\n invalid(`Cannot cancel a batch with status ${batch.status}.`),\n setCorsHeaders,\n );\n return;\n }\n // Idempotent on an already-cancelling batch: no second stamp.\n if (batch.status !== \"cancelling\") {\n batch.status = \"cancelling\";\n batch.cancelling_at = now();\n }\n journalBatches(journal, method, path, flattenHeaders(req.headers), 200);\n writeJson(res, 200, batch, setCorsHeaders);\n}\n"],"mappings":"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"}
@@ -1,9 +1,9 @@
1
1
  const require_helpers = require('./helpers.cjs');
2
- const require_router = require('./router.cjs');
3
2
  const require_sse_writer = require('./sse-writer.cjs');
4
- const require_interruption = require('./interruption.cjs');
5
3
  const require_chaos = require('./chaos.cjs');
4
+ const require_router = require('./router.cjs');
6
5
  const require_recorder = require('./recorder.cjs');
6
+ const require_interruption = require('./interruption.cjs');
7
7
  const require_aws_event_stream = require('./aws-event-stream.cjs');
8
8
 
9
9
  //#region src/bedrock-converse.ts
@@ -132,7 +132,8 @@ function buildBedrockStreamContentWithToolCallsEvents(content, toolCalls, chunkS
132
132
  });
133
133
  blockIndex++;
134
134
  }
135
- const ordered = require_helpers.resolveFixtureBlocks(blocks);
135
+ const outcome = require_helpers.resolveFixtureBlockOutcome(blocks);
136
+ const ordered = outcome.ordered;
136
137
  for (const block of ordered) {
137
138
  if (block.type === "text") {
138
139
  events.push({
@@ -185,7 +186,7 @@ function buildBedrockStreamContentWithToolCallsEvents(content, toolCalls, chunkS
185
186
  }
186
187
  events.push({
187
188
  eventType: "messageStop",
188
- payload: { stopReason: converseStopReason(overrides?.finishReason, "tool_use") }
189
+ payload: { stopReason: converseStopReason(overrides?.finishReason, outcome.hasToolCalls ? "tool_use" : "end_turn") }
189
190
  });
190
191
  events.push({
191
192
  eventType: "metadata",
@@ -443,8 +444,11 @@ function buildConverseContentWithToolCallsResponse(content, toolCalls, logger, r
443
444
  input: argsObj
444
445
  } };
445
446
  };
447
+ let blockHasTools;
446
448
  if (blocks && blocks.length > 0) {
447
- const ordered = require_helpers.resolveFixtureBlocks(blocks);
449
+ const outcome = require_helpers.resolveFixtureBlockOutcome(blocks);
450
+ const ordered = outcome.ordered;
451
+ blockHasTools = outcome.hasToolCalls;
448
452
  for (const block of ordered) if (block.type === "text") contentBlocks.push({ text: block.text });
449
453
  else contentBlocks.push(toolUseBlock({
450
454
  name: block.name,
@@ -460,7 +464,7 @@ function buildConverseContentWithToolCallsResponse(content, toolCalls, logger, r
460
464
  role: "assistant",
461
465
  content: contentBlocks
462
466
  } },
463
- stopReason: converseStopReason(overrides?.finishReason, "tool_use"),
467
+ stopReason: converseStopReason(overrides?.finishReason, blockHasTools === false ? "end_turn" : "tool_use"),
464
468
  usage: converseUsage(overrides),
465
469
  metrics: { latencyMs: 0 }
466
470
  };
@@ -532,7 +536,7 @@ async function handleConverse(req, res, raw, modelId, fixtures, journal, default
532
536
  if (fixture) logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);
533
537
  else logger.debug(`No fixture matched for request`);
534
538
  if (fixture) journal.incrementFixtureMatchCount(fixture, fixtures, testId);
535
- if (require_chaos.applyChaos(res, fixture, defaults.chaos, req.headers, req.url, journal, {
539
+ if (await require_chaos.applyChaosAsync(res, fixture, defaults.chaos, req.headers, req.url, journal, {
536
540
  method: req.method ?? "POST",
537
541
  path: urlPath,
538
542
  headers: require_helpers.flattenHeaders(req.headers),
@@ -758,7 +762,7 @@ async function handleConverseStream(req, res, raw, modelId, fixtures, journal, d
758
762
  if (fixture) logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);
759
763
  else logger.debug(`No fixture matched for request`);
760
764
  if (fixture) journal.incrementFixtureMatchCount(fixture, fixtures, testId);
761
- if (require_chaos.applyChaos(res, fixture, defaults.chaos, req.headers, req.url, journal, {
765
+ if (await require_chaos.applyChaosAsync(res, fixture, defaults.chaos, req.headers, req.url, journal, {
762
766
  method: req.method ?? "POST",
763
767
  path: urlPath,
764
768
  headers: require_helpers.flattenHeaders(req.headers),