@copilotkit/aimock 1.40.0 → 1.42.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 (251) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +28 -1
  4. package/README.md +5 -3
  5. package/dist/a2a-mock.cjs +18 -11
  6. package/dist/a2a-mock.cjs.map +1 -1
  7. package/dist/a2a-mock.d.cts +1 -0
  8. package/dist/a2a-mock.d.cts.map +1 -1
  9. package/dist/a2a-mock.d.ts +1 -0
  10. package/dist/a2a-mock.d.ts.map +1 -1
  11. package/dist/a2a-mock.js +18 -11
  12. package/dist/a2a-mock.js.map +1 -1
  13. package/dist/agui-handler.d.cts.map +1 -1
  14. package/dist/agui-handler.d.ts.map +1 -1
  15. package/dist/agui-mock.cjs +2 -0
  16. package/dist/agui-mock.cjs.map +1 -1
  17. package/dist/agui-mock.d.cts.map +1 -1
  18. package/dist/agui-mock.d.ts.map +1 -1
  19. package/dist/agui-mock.js +2 -0
  20. package/dist/agui-mock.js.map +1 -1
  21. package/dist/bedrock-converse.cjs +36 -2
  22. package/dist/bedrock-converse.cjs.map +1 -1
  23. package/dist/bedrock-converse.d.cts.map +1 -1
  24. package/dist/bedrock-converse.d.ts.map +1 -1
  25. package/dist/bedrock-converse.js +37 -3
  26. package/dist/bedrock-converse.js.map +1 -1
  27. package/dist/bedrock.cjs +50 -13
  28. package/dist/bedrock.cjs.map +1 -1
  29. package/dist/bedrock.d.cts.map +1 -1
  30. package/dist/bedrock.d.ts.map +1 -1
  31. package/dist/bedrock.js +51 -14
  32. package/dist/bedrock.js.map +1 -1
  33. package/dist/byteplus-video.cjs +954 -0
  34. package/dist/byteplus-video.cjs.map +1 -0
  35. package/dist/byteplus-video.d.cts +176 -0
  36. package/dist/byteplus-video.d.cts.map +1 -0
  37. package/dist/byteplus-video.d.ts +176 -0
  38. package/dist/byteplus-video.d.ts.map +1 -0
  39. package/dist/byteplus-video.js +947 -0
  40. package/dist/byteplus-video.js.map +1 -0
  41. package/dist/chaos.cjs +36 -7
  42. package/dist/chaos.cjs.map +1 -1
  43. package/dist/chaos.d.cts +8 -3
  44. package/dist/chaos.d.cts.map +1 -1
  45. package/dist/chaos.d.ts +8 -3
  46. package/dist/chaos.d.ts.map +1 -1
  47. package/dist/chaos.js +36 -8
  48. package/dist/chaos.js.map +1 -1
  49. package/dist/cli.cjs +5 -1
  50. package/dist/cli.cjs.map +1 -1
  51. package/dist/cli.js +5 -1
  52. package/dist/cli.js.map +1 -1
  53. package/dist/cohere.cjs +58 -5
  54. package/dist/cohere.cjs.map +1 -1
  55. package/dist/cohere.d.cts.map +1 -1
  56. package/dist/cohere.d.ts.map +1 -1
  57. package/dist/cohere.js +59 -6
  58. package/dist/cohere.js.map +1 -1
  59. package/dist/config-loader.d.cts.map +1 -1
  60. package/dist/elevenlabs-audio.cjs +36 -2
  61. package/dist/elevenlabs-audio.cjs.map +1 -1
  62. package/dist/elevenlabs-audio.d.cts.map +1 -1
  63. package/dist/elevenlabs-audio.d.ts.map +1 -1
  64. package/dist/elevenlabs-audio.js +37 -3
  65. package/dist/elevenlabs-audio.js.map +1 -1
  66. package/dist/embeddings.cjs +58 -3
  67. package/dist/embeddings.cjs.map +1 -1
  68. package/dist/embeddings.d.cts.map +1 -1
  69. package/dist/embeddings.d.ts.map +1 -1
  70. package/dist/embeddings.js +59 -4
  71. package/dist/embeddings.js.map +1 -1
  72. package/dist/fal-audio.cjs +38 -2
  73. package/dist/fal-audio.cjs.map +1 -1
  74. package/dist/fal-audio.d.cts.map +1 -1
  75. package/dist/fal-audio.d.ts.map +1 -1
  76. package/dist/fal-audio.js +39 -3
  77. package/dist/fal-audio.js.map +1 -1
  78. package/dist/fixture-loader.cjs +0 -0
  79. package/dist/fixture-loader.cjs.map +1 -1
  80. package/dist/fixture-loader.d.cts +9 -1
  81. package/dist/fixture-loader.d.cts.map +1 -1
  82. package/dist/fixture-loader.d.ts +9 -1
  83. package/dist/fixture-loader.d.ts.map +1 -1
  84. package/dist/fixture-loader.js +0 -0
  85. package/dist/fixture-loader.js.map +1 -1
  86. package/dist/gemini-embeddings.cjs +19 -1
  87. package/dist/gemini-embeddings.cjs.map +1 -1
  88. package/dist/gemini-embeddings.js +20 -2
  89. package/dist/gemini-embeddings.js.map +1 -1
  90. package/dist/gemini-interactions.cjs +15 -1
  91. package/dist/gemini-interactions.cjs.map +1 -1
  92. package/dist/gemini-interactions.d.cts.map +1 -1
  93. package/dist/gemini-interactions.d.ts.map +1 -1
  94. package/dist/gemini-interactions.js +16 -2
  95. package/dist/gemini-interactions.js.map +1 -1
  96. package/dist/gemini.cjs +70 -6
  97. package/dist/gemini.cjs.map +1 -1
  98. package/dist/gemini.d.cts.map +1 -1
  99. package/dist/gemini.d.ts.map +1 -1
  100. package/dist/gemini.js +71 -7
  101. package/dist/gemini.js.map +1 -1
  102. package/dist/grok-video.cjs +2 -2
  103. package/dist/grok-video.cjs.map +1 -1
  104. package/dist/grok-video.d.cts.map +1 -1
  105. package/dist/grok-video.d.ts.map +1 -1
  106. package/dist/grok-video.js +2 -2
  107. package/dist/grok-video.js.map +1 -1
  108. package/dist/helpers.cjs +160 -5
  109. package/dist/helpers.cjs.map +1 -1
  110. package/dist/helpers.d.cts.map +1 -1
  111. package/dist/helpers.d.ts.map +1 -1
  112. package/dist/helpers.js +152 -6
  113. package/dist/helpers.js.map +1 -1
  114. package/dist/images.cjs +24 -6
  115. package/dist/images.cjs.map +1 -1
  116. package/dist/images.d.cts +11 -3
  117. package/dist/images.d.cts.map +1 -1
  118. package/dist/images.d.ts +11 -3
  119. package/dist/images.d.ts.map +1 -1
  120. package/dist/images.js +25 -7
  121. package/dist/images.js.map +1 -1
  122. package/dist/index.cjs +7 -0
  123. package/dist/index.d.cts +2 -1
  124. package/dist/index.d.ts +2 -1
  125. package/dist/index.js +2 -1
  126. package/dist/llmock.cjs +15 -0
  127. package/dist/llmock.cjs.map +1 -1
  128. package/dist/llmock.d.cts +12 -0
  129. package/dist/llmock.d.cts.map +1 -1
  130. package/dist/llmock.d.ts +12 -0
  131. package/dist/llmock.d.ts.map +1 -1
  132. package/dist/llmock.js +16 -1
  133. package/dist/llmock.js.map +1 -1
  134. package/dist/mcp-mock.cjs +11 -0
  135. package/dist/mcp-mock.cjs.map +1 -1
  136. package/dist/mcp-mock.d.cts.map +1 -1
  137. package/dist/mcp-mock.d.ts.map +1 -1
  138. package/dist/mcp-mock.js +11 -0
  139. package/dist/mcp-mock.js.map +1 -1
  140. package/dist/messages.cjs +18 -1
  141. package/dist/messages.cjs.map +1 -1
  142. package/dist/messages.d.cts.map +1 -1
  143. package/dist/messages.d.ts.map +1 -1
  144. package/dist/messages.js +19 -2
  145. package/dist/messages.js.map +1 -1
  146. package/dist/metrics.cjs +20 -0
  147. package/dist/metrics.cjs.map +1 -1
  148. package/dist/metrics.d.cts.map +1 -1
  149. package/dist/metrics.d.ts.map +1 -1
  150. package/dist/metrics.js +19 -1
  151. package/dist/metrics.js.map +1 -1
  152. package/dist/moderation.cjs +67 -3
  153. package/dist/moderation.cjs.map +1 -1
  154. package/dist/moderation.d.cts +1 -0
  155. package/dist/moderation.d.cts.map +1 -1
  156. package/dist/moderation.d.ts +1 -0
  157. package/dist/moderation.d.ts.map +1 -1
  158. package/dist/moderation.js +68 -4
  159. package/dist/moderation.js.map +1 -1
  160. package/dist/ollama.cjs +75 -5
  161. package/dist/ollama.cjs.map +1 -1
  162. package/dist/ollama.d.cts.map +1 -1
  163. package/dist/ollama.d.ts.map +1 -1
  164. package/dist/ollama.js +76 -6
  165. package/dist/ollama.js.map +1 -1
  166. package/dist/openrouter-video.cjs +4 -4
  167. package/dist/openrouter-video.cjs.map +1 -1
  168. package/dist/openrouter-video.d.cts.map +1 -1
  169. package/dist/openrouter-video.d.ts.map +1 -1
  170. package/dist/openrouter-video.js +4 -4
  171. package/dist/openrouter-video.js.map +1 -1
  172. package/dist/provider-auth.cjs +2 -0
  173. package/dist/provider-auth.cjs.map +1 -1
  174. package/dist/provider-auth.js +2 -0
  175. package/dist/provider-auth.js.map +1 -1
  176. package/dist/rerank.cjs +65 -2
  177. package/dist/rerank.cjs.map +1 -1
  178. package/dist/rerank.d.cts +1 -1
  179. package/dist/rerank.d.cts.map +1 -1
  180. package/dist/rerank.d.ts +1 -1
  181. package/dist/rerank.d.ts.map +1 -1
  182. package/dist/rerank.js +66 -3
  183. package/dist/rerank.js.map +1 -1
  184. package/dist/responses.cjs +18 -1
  185. package/dist/responses.cjs.map +1 -1
  186. package/dist/responses.d.cts.map +1 -1
  187. package/dist/responses.d.ts.map +1 -1
  188. package/dist/responses.js +19 -2
  189. package/dist/responses.js.map +1 -1
  190. package/dist/router.cjs +1 -0
  191. package/dist/router.cjs.map +1 -1
  192. package/dist/router.d.cts.map +1 -1
  193. package/dist/router.d.ts.map +1 -1
  194. package/dist/router.js +1 -0
  195. package/dist/router.js.map +1 -1
  196. package/dist/search.cjs +67 -3
  197. package/dist/search.cjs.map +1 -1
  198. package/dist/search.d.cts +1 -0
  199. package/dist/search.d.cts.map +1 -1
  200. package/dist/search.d.ts +1 -0
  201. package/dist/search.d.ts.map +1 -1
  202. package/dist/search.js +68 -4
  203. package/dist/search.js.map +1 -1
  204. package/dist/server.cjs +406 -17
  205. package/dist/server.cjs.map +1 -1
  206. package/dist/server.d.cts +2 -0
  207. package/dist/server.d.cts.map +1 -1
  208. package/dist/server.d.ts +2 -0
  209. package/dist/server.d.ts.map +1 -1
  210. package/dist/server.js +410 -21
  211. package/dist/server.js.map +1 -1
  212. package/dist/speech.cjs +18 -1
  213. package/dist/speech.cjs.map +1 -1
  214. package/dist/speech.d.cts.map +1 -1
  215. package/dist/speech.d.ts.map +1 -1
  216. package/dist/speech.js +19 -2
  217. package/dist/speech.js.map +1 -1
  218. package/dist/stream-collapse.cjs +2 -1
  219. package/dist/stream-collapse.cjs.map +1 -1
  220. package/dist/stream-collapse.js +2 -1
  221. package/dist/stream-collapse.js.map +1 -1
  222. package/dist/transcription.cjs +21 -2
  223. package/dist/transcription.cjs.map +1 -1
  224. package/dist/transcription.d.cts.map +1 -1
  225. package/dist/transcription.d.ts.map +1 -1
  226. package/dist/transcription.js +21 -2
  227. package/dist/transcription.js.map +1 -1
  228. package/dist/types.d.cts +58 -5
  229. package/dist/types.d.cts.map +1 -1
  230. package/dist/types.d.ts +58 -5
  231. package/dist/types.d.ts.map +1 -1
  232. package/dist/vector-mock.cjs +88 -0
  233. package/dist/vector-mock.cjs.map +1 -1
  234. package/dist/vector-mock.d.cts.map +1 -1
  235. package/dist/vector-mock.d.ts.map +1 -1
  236. package/dist/vector-mock.js +89 -1
  237. package/dist/vector-mock.js.map +1 -1
  238. package/dist/vector-types.d.cts.map +1 -1
  239. package/dist/veo-video.cjs +2 -2
  240. package/dist/veo-video.cjs.map +1 -1
  241. package/dist/veo-video.d.cts.map +1 -1
  242. package/dist/veo-video.d.ts.map +1 -1
  243. package/dist/veo-video.js +2 -2
  244. package/dist/veo-video.js.map +1 -1
  245. package/dist/video.cjs +19 -2
  246. package/dist/video.cjs.map +1 -1
  247. package/dist/video.d.cts.map +1 -1
  248. package/dist/video.d.ts.map +1 -1
  249. package/dist/video.js +20 -3
  250. package/dist/video.js.map +1 -1
  251. package/package.json +2 -2
package/dist/server.cjs CHANGED
@@ -1,4 +1,5 @@
1
1
  const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
2
+ const require_constants = require('./constants.cjs');
2
3
  const require_api_key_auth = require('./api-key-auth.cjs');
3
4
  const require_helpers = require('./helpers.cjs');
4
5
  const require_logger = require('./logger.cjs');
@@ -27,6 +28,7 @@ const require_fal = require('./fal.cjs');
27
28
  const require_openrouter_video = require('./openrouter-video.cjs');
28
29
  const require_veo_video = require('./veo-video.cjs');
29
30
  const require_grok_video = require('./grok-video.cjs');
31
+ const require_byteplus_video = require('./byteplus-video.cjs');
30
32
  const require_elevenlabs_audio = require('./elevenlabs-audio.cjs');
31
33
  const require_ollama = require('./ollama.cjs');
32
34
  const require_cohere = require('./cohere.cjs');
@@ -126,7 +128,8 @@ const DEFAULT_MODELS = [
126
128
  const CORS_HEADERS = {
127
129
  "Access-Control-Allow-Origin": "*",
128
130
  "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
129
- "Access-Control-Allow-Headers": "*"
131
+ "Access-Control-Allow-Headers": "*",
132
+ "Access-Control-Expose-Headers": "X-Total-Count"
130
133
  };
131
134
  function setCorsHeaders(res) {
132
135
  for (const [key, value] of Object.entries(CORS_HEADERS)) res.setHeader(key, value);
@@ -144,11 +147,24 @@ function handleNotFound(res, message) {
144
147
  } }));
145
148
  }
146
149
  const CONTROL_PREFIX = "/__aimock";
150
+ /** The complete `GET /__aimock/fixtures` query-param vocabulary; anything else 400s. */
151
+ const FIXTURES_PARAMS = new Set(["include"]);
152
+ /** The complete `GET /__aimock/journal` query-param vocabulary; anything else 400s. */
153
+ const JOURNAL_PARAMS = new Set([
154
+ "limit",
155
+ "offset",
156
+ "path",
157
+ "method",
158
+ "status",
159
+ "service",
160
+ "testId"
161
+ ]);
147
162
  /**
148
163
  * Perform a full reset: clear the fixtures array, the journal (entries *and*
149
164
  * per-test fixture match-counts, i.e. sequence position), the video and fal.ai
150
- * job/queue state, and the Gemini interaction/event-id counters, then re-zero
151
- * the `aimock_fixtures_loaded` gauge.
165
+ * job/queue state, the Gemini interaction/event-id counters, and any runtime
166
+ * chaos override (reverting to the construction-time chaos config), then
167
+ * re-zero the `aimock_fixtures_loaded` gauge.
152
168
  *
153
169
  * `targets` is `null` when no server is running (an in-process `reset()` before
154
170
  * `start()`). The process-global generation state is reset either way, since it
@@ -165,17 +181,112 @@ function performFullReset(fixtures, targets) {
165
181
  require_gemini_interactions.resetEventIdCounter();
166
182
  if (!targets) return;
167
183
  targets.journal.clear();
184
+ targets.defaults.chaos = void 0;
168
185
  targets.videoStates.clear();
169
186
  targets.openRouterVideoJobs.clear();
170
187
  targets.veoVideoJobs.clear();
171
188
  targets.grokVideoJobs.clear();
189
+ targets.bytePlusVideoJobs.clear();
172
190
  if (targets.defaults.registry) targets.defaults.registry.setGauge("aimock_fixtures_loaded", {}, fixtures.length);
173
191
  }
174
192
  /**
193
+ * JSON-safe redaction for control-API inspection output. Functions (fixture
194
+ * `predicate`s, response factories) and RegExps do not survive
195
+ * JSON.stringify as anything useful, so they become marker strings.
196
+ */
197
+ function redactForInspection(value) {
198
+ if (typeof value === "function") return "[function]";
199
+ if (value instanceof RegExp) return String(value);
200
+ if (Array.isArray(value)) return value.map(redactForInspection);
201
+ if (value !== null && typeof value === "object") {
202
+ const out = {};
203
+ for (const [key, entry] of Object.entries(value)) out[key] = redactForInspection(entry);
204
+ return out;
205
+ }
206
+ return value;
207
+ }
208
+ /**
209
+ * One-line kind label for a fixture response used by
210
+ * `GET /__aimock/fixtures?include=fixtures` (e.g. "text", "error").
211
+ * Response factories are reported as "factory".
212
+ *
213
+ * Discriminates with the same `is*Response` guards the request handlers use,
214
+ * in the same order — NOT by inspecting keys. Every response shape extends
215
+ * `ResponseOverrides` (`id`/`created`/`model`/`usage`), so a recorded fixture's
216
+ * first key is usually an envelope override rather than the discriminant. The
217
+ * audio-before-content/toolCalls order is the ORDERING CONTRACT documented on
218
+ * `AudioResponse` in types.ts: those shapes structurally overlap.
219
+ */
220
+ function fixtureResponseKind(response) {
221
+ if (require_helpers.isResponseFactory(response)) return "factory";
222
+ if (require_helpers.isErrorResponse(response)) return "error";
223
+ if (require_helpers.isAudioResponse(response)) return "audio";
224
+ if (require_helpers.isVideoResponse(response)) return "video";
225
+ if (require_helpers.isTranscriptionResponse(response)) return "transcription";
226
+ if (require_helpers.isImageResponse(response)) return "image";
227
+ if (require_helpers.isEmbeddingResponse(response)) return "embedding";
228
+ if (require_helpers.isJSONResponse(response)) return "json";
229
+ if (require_helpers.isContentWithToolCallsResponse(response)) return "contentWithToolCalls";
230
+ if (require_helpers.isToolCallResponse(response)) return "toolCalls";
231
+ if (require_helpers.isTextResponse(response)) return "text";
232
+ return "unknown";
233
+ }
234
+ /**
235
+ * Resolve a journal entry's testId exactly the way `getTestId` resolves an
236
+ * incoming request's: the `X-Test-Id` header wins, then `?testId=` parsed out
237
+ * of the query string, then `DEFAULT_TEST_ID`. The
238
+ * filter and the server's own per-test isolation must never disagree — and a
239
+ * raw `path.includes("testId=t1")` both prefix-collides with `t10` and matches
240
+ * unrelated params like `notTestId`.
241
+ */
242
+ function journalEntryTestId(entry) {
243
+ const header = entry.headers["x-test-id"];
244
+ if (header) return header;
245
+ const qIdx = entry.path.indexOf("?");
246
+ if (qIdx !== -1) {
247
+ const queryValue = new URLSearchParams(entry.path.slice(qIdx + 1)).get("testId");
248
+ if (queryValue) return queryValue;
249
+ }
250
+ return require_constants.DEFAULT_TEST_ID;
251
+ }
252
+ /**
253
+ * The testId a chaos override is stored under, and read back with — resolved by
254
+ * `getTestId`, i.e. `X-Test-Id` header then `?testId=`, exactly as
255
+ * `resolveScopedDefaults` resolves the traffic being evaluated. A control call
256
+ * and the requests it is meant to affect can then never land in different
257
+ * scopes. `DEFAULT_TEST_ID` (neither header nor param) means the server-wide
258
+ * baseline.
259
+ *
260
+ * Returns `null` when `X-Test-Id` is PRESENT but blank. `String(testId ?? "")`
261
+ * in a harness produces that trivially, and silently treating it as "untagged"
262
+ * would install a server-wide baseline that fails every other test — the exact
263
+ * cross-test leak per-testId scoping exists to prevent. Callers must 400.
264
+ */
265
+ function chaosScopeId(req) {
266
+ const headerValue = req.headers["x-test-id"];
267
+ const raw = Array.isArray(headerValue) ? headerValue[0] : headerValue;
268
+ if (typeof raw === "string" && raw.trim() === "") return null;
269
+ return require_helpers.getTestId(req);
270
+ }
271
+ /** 400 for a control call whose `X-Test-Id` is present but blank. */
272
+ function writeBlankTestId(res) {
273
+ res.writeHead(400, { "Content-Type": "application/json" });
274
+ res.end(JSON.stringify({ error: "Invalid 'X-Test-Id': header is present but empty" }));
275
+ return true;
276
+ }
277
+ /** The chaos config in effect for one testId: its override, else the baseline. */
278
+ function effectiveChaos(defaults, scopeId) {
279
+ const scoped = defaults.chaosByTestId?.get(scopeId);
280
+ if (scoped) return scoped;
281
+ const current = defaults.chaos;
282
+ if (!current) return {};
283
+ return require_chaos.isChaosScope(current) ? current.base ?? {} : current;
284
+ }
285
+ /**
175
286
  * Handle requests under `/__aimock/`. Returns `true` if the request was
176
287
  * handled, `false` if the path doesn't match the control prefix.
177
288
  */
178
- async function handleControlAPI(req, res, pathname, fixtures, journal, videoStates, openRouterVideoJobs, veoVideoJobs, grokVideoJobs, defaults) {
289
+ async function handleControlAPI(req, res, pathname, searchParams, fixtures, journal, videoStates, openRouterVideoJobs, veoVideoJobs, grokVideoJobs, bytePlusVideoJobs, defaults) {
179
290
  if (!pathname.startsWith(CONTROL_PREFIX)) return false;
180
291
  const subPath = pathname.slice(9);
181
292
  setCorsHeaders(res);
@@ -185,13 +296,148 @@ async function handleControlAPI(req, res, pathname, fixtures, journal, videoStat
185
296
  return true;
186
297
  }
187
298
  if (subPath === "/journal" && req.method === "GET") {
188
- res.writeHead(200, { "Content-Type": "application/json" });
189
- res.end(JSON.stringify(journal.getAll()));
299
+ const bad = (message) => {
300
+ res.writeHead(400, { "Content-Type": "application/json" });
301
+ res.end(JSON.stringify({ error: message }));
302
+ return true;
303
+ };
304
+ for (const key of searchParams.keys()) if (!JOURNAL_PARAMS.has(key)) return bad(`Unknown query parameter: '${key}'. Supported: ${[...JOURNAL_PARAMS].join(", ")}`);
305
+ const parseNonNegativeInt = (name, fallback) => {
306
+ const raw = searchParams.get(name);
307
+ if (raw === null) return fallback;
308
+ if (!/^\d+$/.test(raw.trim())) return null;
309
+ return Number(raw);
310
+ };
311
+ const limit = parseNonNegativeInt("limit", -1);
312
+ const offset = parseNonNegativeInt("offset", 0);
313
+ if (limit === null) return bad("Invalid 'limit': must be an integer >= 0");
314
+ if (offset === null) return bad("Invalid 'offset': must be an integer >= 0");
315
+ let statusFilter;
316
+ const statusRaw = searchParams.get("status");
317
+ if (statusRaw !== null) {
318
+ if (!/^\d+$/.test(statusRaw.trim())) return bad("Invalid 'status': must be an integer");
319
+ statusFilter = Number(statusRaw);
320
+ }
321
+ const pathFilter = searchParams.get("path");
322
+ const methodFilter = searchParams.get("method");
323
+ const serviceFilter = searchParams.get("service");
324
+ const testIdFilter = searchParams.get("testId");
325
+ let entries = journal.getAll();
326
+ if (methodFilter !== null) {
327
+ const want = methodFilter.toUpperCase();
328
+ entries = entries.filter((e) => e.method.toUpperCase() === want);
329
+ }
330
+ if (pathFilter !== null) entries = entries.filter((e) => e.path.includes(pathFilter));
331
+ if (statusFilter !== void 0) entries = entries.filter((e) => e.response.status === statusFilter);
332
+ if (serviceFilter !== null) entries = entries.filter((e) => e.service === serviceFilter);
333
+ if (testIdFilter !== null) entries = entries.filter((e) => journalEntryTestId(e) === testIdFilter);
334
+ const total = entries.length;
335
+ if (offset > 0) entries = entries.slice(offset);
336
+ if (limit >= 0) entries = entries.slice(0, limit);
337
+ res.writeHead(200, {
338
+ "Content-Type": "application/json",
339
+ "X-Total-Count": String(total)
340
+ });
341
+ res.end(JSON.stringify(entries));
190
342
  return true;
191
343
  }
192
344
  if (subPath === "/fixtures" && req.method === "GET") {
345
+ for (const key of searchParams.keys()) if (!FIXTURES_PARAMS.has(key)) {
346
+ res.writeHead(400, { "Content-Type": "application/json" });
347
+ res.end(JSON.stringify({ error: `Unknown query parameter: '${key}'. Supported: ${[...FIXTURES_PARAMS].join(", ")}` }));
348
+ return true;
349
+ }
350
+ const include = searchParams.get("include");
351
+ if (include === null) {
352
+ res.writeHead(200, { "Content-Type": "application/json" });
353
+ res.end(JSON.stringify({ count: fixtures.length }));
354
+ return true;
355
+ }
356
+ if (include !== "fixtures") {
357
+ res.writeHead(400, { "Content-Type": "application/json" });
358
+ res.end(JSON.stringify({ error: "Invalid 'include': expected 'fixtures'" }));
359
+ return true;
360
+ }
361
+ res.writeHead(200, { "Content-Type": "application/json" });
362
+ res.end(JSON.stringify({
363
+ count: fixtures.length,
364
+ fixtures: fixtures.map((fixture, index) => ({
365
+ index,
366
+ match: redactForInspection(fixture.match),
367
+ responseKind: fixtureResponseKind(fixture.response),
368
+ ...fixture.latency !== void 0 ? { latency: fixture.latency } : {},
369
+ ...fixture.chaos !== void 0 ? { chaos: fixture.chaos } : {}
370
+ }))
371
+ }));
372
+ return true;
373
+ }
374
+ if (subPath === "/chaos" && req.method === "GET") {
375
+ const scopeId = chaosScopeId(req);
376
+ if (scopeId === null) return writeBlankTestId(res);
193
377
  res.writeHead(200, { "Content-Type": "application/json" });
194
- res.end(JSON.stringify({ count: fixtures.length }));
378
+ res.end(JSON.stringify({ chaos: effectiveChaos(defaults, scopeId) }));
379
+ return true;
380
+ }
381
+ if (subPath === "/chaos" && req.method === "DELETE") {
382
+ const scopeId = chaosScopeId(req);
383
+ if (scopeId === null) return writeBlankTestId(res);
384
+ defaults.chaosByTestId?.delete(scopeId);
385
+ res.writeHead(200, { "Content-Type": "application/json" });
386
+ res.end(JSON.stringify({ chaos: effectiveChaos(defaults, scopeId) }));
387
+ return true;
388
+ }
389
+ if (subPath === "/chaos" && req.method === "POST") {
390
+ const scopeId = chaosScopeId(req);
391
+ if (scopeId === null) return writeBlankTestId(res);
392
+ let raw;
393
+ try {
394
+ raw = await require_helpers.readBody(req);
395
+ } catch (err) {
396
+ const msg = err instanceof Error ? err.message : String(err);
397
+ defaults.logger.error(`POST /__aimock/chaos: failed to read body: ${msg}`);
398
+ res.writeHead(400, { "Content-Type": "application/json" });
399
+ res.end(JSON.stringify({ error: `Failed to read request body: ${msg}` }));
400
+ return true;
401
+ }
402
+ let parsed;
403
+ try {
404
+ parsed = JSON.parse(raw);
405
+ } catch (err) {
406
+ const msg = err instanceof Error ? err.message : String(err);
407
+ defaults.logger.error(`POST /__aimock/chaos: invalid JSON: ${msg}`);
408
+ res.writeHead(400, { "Content-Type": "application/json" });
409
+ res.end(JSON.stringify({ error: `Invalid JSON: ${msg}` }));
410
+ return true;
411
+ }
412
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
413
+ res.writeHead(400, { "Content-Type": "application/json" });
414
+ res.end(JSON.stringify({ error: "Invalid body: expected a JSON object" }));
415
+ return true;
416
+ }
417
+ const allowed = [
418
+ "dropRate",
419
+ "malformedRate",
420
+ "disconnectRate"
421
+ ];
422
+ for (const key of Object.keys(parsed)) if (!allowed.includes(key)) {
423
+ res.writeHead(400, { "Content-Type": "application/json" });
424
+ res.end(JSON.stringify({ error: `Unknown chaos field: '${key}'` }));
425
+ return true;
426
+ }
427
+ const next = {};
428
+ for (const key of allowed) {
429
+ const value = parsed[key];
430
+ if (value === void 0) continue;
431
+ if (typeof value !== "number" || Number.isNaN(value) || value < 0 || value > 1) {
432
+ res.writeHead(400, { "Content-Type": "application/json" });
433
+ res.end(JSON.stringify({ error: `Invalid '${key}': must be a number between 0 and 1` }));
434
+ return true;
435
+ }
436
+ next[key] = value;
437
+ }
438
+ defaults.chaosByTestId?.set(scopeId, next);
439
+ res.writeHead(200, { "Content-Type": "application/json" });
440
+ res.end(JSON.stringify({ chaos: effectiveChaos(defaults, scopeId) }));
195
441
  return true;
196
442
  }
197
443
  if (subPath === "/fixtures" && req.method === "POST") {
@@ -249,6 +495,7 @@ async function handleControlAPI(req, res, pathname, fixtures, journal, videoStat
249
495
  openRouterVideoJobs,
250
496
  veoVideoJobs,
251
497
  grokVideoJobs,
498
+ bytePlusVideoJobs,
252
499
  defaults
253
500
  });
254
501
  if (subPath === "/reset" && req.method === "POST") {
@@ -299,8 +546,37 @@ async function handleControlAPI(req, res, pathname, fixtures, journal, videoStat
299
546
  res.end(JSON.stringify({ error: `Invalid JSON: ${msg}` }));
300
547
  return true;
301
548
  }
549
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
550
+ res.writeHead(400, { "Content-Type": "application/json" });
551
+ res.end(JSON.stringify({ error: "Invalid body: expected a JSON object" }));
552
+ return true;
553
+ }
302
554
  const status = parsed.status ?? 500;
555
+ if (!require_fixture_loader.isInjectableStatus(status)) {
556
+ res.writeHead(400, { "Content-Type": "application/json" });
557
+ res.end(JSON.stringify({ error: `Invalid 'status': must be ${require_fixture_loader.INJECTED_STATUS_RANGE}` }));
558
+ return true;
559
+ }
303
560
  const errorBody = parsed.body;
561
+ if (errorBody !== void 0 && errorBody !== null) {
562
+ if (typeof errorBody !== "object" || Array.isArray(errorBody)) {
563
+ res.writeHead(400, { "Content-Type": "application/json" });
564
+ res.end(JSON.stringify({ error: "Invalid 'body': must be an object" }));
565
+ return true;
566
+ }
567
+ for (const field of [
568
+ "message",
569
+ "type",
570
+ "code"
571
+ ]) {
572
+ const value = errorBody[field];
573
+ if (value !== void 0 && value !== null && typeof value !== "string") {
574
+ res.writeHead(400, { "Content-Type": "application/json" });
575
+ res.end(JSON.stringify({ error: `Invalid 'body.${field}': must be a string` }));
576
+ return true;
577
+ }
578
+ }
579
+ }
304
580
  const errorFixture = {
305
581
  match: { predicate: () => true },
306
582
  response: {
@@ -360,7 +636,6 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
360
636
  let body;
361
637
  try {
362
638
  body = JSON.parse(raw);
363
- if (modelFallback && !body.model) body.model = modelFallback;
364
639
  } catch (parseErr) {
365
640
  const detail = parseErr instanceof Error ? parseErr.message : "unknown parse error";
366
641
  journal.add({
@@ -381,6 +656,24 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
381
656
  } }));
382
657
  return;
383
658
  }
659
+ if (!require_helpers.isJsonObject(body)) {
660
+ journal.add({
661
+ method: req.method ?? "POST",
662
+ path: req.url ?? COMPLETIONS_PATH,
663
+ headers: require_helpers.flattenHeaders(req.headers),
664
+ body: null,
665
+ response: {
666
+ status: 400,
667
+ fixture: null
668
+ }
669
+ });
670
+ require_sse_writer.writeErrorResponse(res, 400, openRouter ? require_openrouter_chat.serializeOpenRouterError(400, "Request body must be a JSON object") : JSON.stringify({ error: {
671
+ message: "Request body must be a JSON object",
672
+ type: "invalid_request_error"
673
+ } }));
674
+ return;
675
+ }
676
+ if (modelFallback && !body.model) body.model = modelFallback;
384
677
  if (!Array.isArray(body.messages)) {
385
678
  journal.add({
386
679
  method: req.method ?? "POST",
@@ -472,7 +765,7 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
472
765
  const snippet = typeof lastUserMsg?.content === "string" ? lastUserMsg.content.slice(0, 80) : "";
473
766
  defaults.logger.debug(`No fixture matched for request (model=${body.model ?? "?"}, msg="${snippet}")`);
474
767
  }
475
- const chaosAction = require_chaos.evaluateChaos(fixture, defaults.chaos, req.headers, defaults.logger);
768
+ const chaosAction = require_chaos.evaluateChaos(fixture, defaults.chaos, req.headers, defaults.logger, req.url);
476
769
  const chaosContext = {
477
770
  method,
478
771
  path,
@@ -769,13 +1062,27 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
769
1062
  const logger = new require_logger.Logger(options?.logLevel ?? "silent");
770
1063
  const registry = options?.metrics ? require_metrics.createMetricsRegistry() : void 0;
771
1064
  const serverOptions = options ?? {};
1065
+ const chaosByTestId = /* @__PURE__ */ new Map();
772
1066
  const defaults = {
773
1067
  latency: serverOptions.latency ?? 0,
774
1068
  chunkSize: Math.max(1, serverOptions.chunkSize ?? DEFAULT_CHUNK_SIZE),
775
1069
  replaySpeed: serverOptions.replaySpeed ?? 1,
776
1070
  logger,
1071
+ chaosByTestId,
777
1072
  get chaos() {
778
- return serverOptions.chaos;
1073
+ return {
1074
+ base: chaosByTestId.get(require_constants.DEFAULT_TEST_ID) ?? serverOptions.chaos,
1075
+ byTestId: chaosByTestId
1076
+ };
1077
+ },
1078
+ set chaos(value) {
1079
+ if (value === void 0) {
1080
+ chaosByTestId.clear();
1081
+ return;
1082
+ }
1083
+ const base = require_chaos.isChaosScope(value) ? value.base : value;
1084
+ if (base === void 0) chaosByTestId.delete(require_constants.DEFAULT_TEST_ID);
1085
+ else chaosByTestId.set(require_constants.DEFAULT_TEST_ID, base);
779
1086
  },
780
1087
  registry,
781
1088
  get record() {
@@ -798,6 +1105,9 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
798
1105
  },
799
1106
  get grokVideo() {
800
1107
  return serverOptions.grokVideo;
1108
+ },
1109
+ get bytePlusVideo() {
1110
+ return serverOptions.bytePlusVideo;
801
1111
  }
802
1112
  };
803
1113
  if (options?.chaos) {
@@ -833,6 +1143,10 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
833
1143
  {
834
1144
  name: "grokVideo",
835
1145
  config: options?.grokVideo
1146
+ },
1147
+ {
1148
+ name: "bytePlusVideo",
1149
+ config: options?.bytePlusVideo
836
1150
  }
837
1151
  ]) {
838
1152
  if (!config) continue;
@@ -847,6 +1161,16 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
847
1161
  const cap = options?.record?.openRouterVideo?.maxContentBytes;
848
1162
  if (cap !== void 0 && (!Number.isInteger(cap) || cap < 0)) logger.warn(`record.openRouterVideo.maxContentBytes (${cap}) is not a non-negative integer — using the default cap (${require_openrouter_video.OPENROUTER_VIDEO_DEFAULT_MAX_CONTENT_BYTES})`);
849
1163
  }
1164
+ {
1165
+ const bytePlusBase = options?.record?.providers?.byteplus;
1166
+ if (bytePlusBase !== void 0) {
1167
+ let basePath;
1168
+ try {
1169
+ basePath = new URL(bytePlusBase).pathname;
1170
+ } catch {}
1171
+ if (basePath !== void 0 && /(^|\/)api\/v3\/?$/.test(basePath)) throw new Error(`record.providers.byteplus (${bytePlusBase}) ends in /api/v3, but aimock appends the /api/v3 prefix itself — every BytePlus request would go to /api/v3/api/v3/... and fail upstream. Configure the ORIGIN only (e.g. https://ark.ap-southeast.bytepluses.com).`);
1172
+ }
1173
+ }
850
1174
  const journal = new require_journal.Journal({
851
1175
  maxEntries: options?.journalMaxEntries ?? 1e3,
852
1176
  fixtureCountsMaxTestIds: options?.fixtureCountsMaxTestIds ?? 500
@@ -855,6 +1179,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
855
1179
  const openRouterVideoJobs = new require_openrouter_video.OpenRouterVideoJobMap();
856
1180
  const veoVideoJobs = new require_veo_video.VeoVideoJobMap();
857
1181
  const grokVideoJobs = new require_grok_video.GrokVideoJobMap();
1182
+ const bytePlusVideoJobs = new require_byteplus_video.BytePlusVideoJobMap();
858
1183
  if (mounts) for (const { handler } of mounts) {
859
1184
  if (handler.setJournal) handler.setJournal(journal);
860
1185
  if (registry && handler.setRegistry) handler.setRegistry(registry);
@@ -880,7 +1205,9 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
880
1205
  const startTime = registry ? process.hrtime.bigint() : 0n;
881
1206
  const parsedUrl = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
882
1207
  let pathname = parsedUrl.pathname;
883
- const isOpenRouter = require_openrouter_chat.isOpenRouterPath(pathname);
1208
+ const originalPathname = pathname;
1209
+ const isOpenRouter = require_openrouter_chat.isOpenRouterPath(originalPathname);
1210
+ const isBytePlusArk = defaults.record?.providers.byteplus !== void 0 && originalPathname.startsWith("/api/v3/");
884
1211
  if (registry) res.on("finish", () => {
885
1212
  try {
886
1213
  const normalizedPath = require_metrics.normalizePathLabel(pathname);
@@ -912,7 +1239,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
912
1239
  require_api_key_auth.markAuthenticatedRequest(req, resolvedAuth.policy);
913
1240
  }
914
1241
  if (pathname.startsWith(CONTROL_PREFIX)) {
915
- await handleControlAPI(req, res, pathname, fixtures, journal, videoStates, openRouterVideoJobs, veoVideoJobs, grokVideoJobs, defaults);
1242
+ await handleControlAPI(req, res, pathname, parsedUrl.searchParams, fixtures, journal, videoStates, openRouterVideoJobs, veoVideoJobs, grokVideoJobs, bytePlusVideoJobs, defaults);
916
1243
  return;
917
1244
  }
918
1245
  if (mounts) {
@@ -1095,6 +1422,66 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1095
1422
  }
1096
1423
  return;
1097
1424
  }
1425
+ const bytePlusVideoStatusMatch = pathname.match(require_metrics.BYTEPLUS_VIDEO_STATUS_RE);
1426
+ if (bytePlusVideoStatusMatch && req.method === "GET") {
1427
+ try {
1428
+ await require_byteplus_video.handleBytePlusVideoStatus(req, res, bytePlusVideoStatusMatch[1], fixtures, journal, defaults, setCorsHeaders, bytePlusVideoJobs);
1429
+ } catch (err) {
1430
+ const msg = err instanceof Error ? err.message : "Internal error";
1431
+ defaults.logger.error(`byteplus-video status: ${msg}`);
1432
+ if (!res.headersSent) {
1433
+ try {
1434
+ journal.add({
1435
+ method: req.method ?? "GET",
1436
+ path: req.url ?? pathname,
1437
+ headers: require_helpers.flattenHeaders(req.headers),
1438
+ body: null,
1439
+ response: {
1440
+ status: 500,
1441
+ fixture: null
1442
+ }
1443
+ });
1444
+ } catch (jErr) {
1445
+ defaults.logger.warn(`byteplus-video status: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
1446
+ }
1447
+ require_sse_writer.writeErrorResponse(res, 500, JSON.stringify({ error: {
1448
+ message: msg,
1449
+ type: "server_error"
1450
+ } }));
1451
+ } else if (!res.writableEnded) res.destroy();
1452
+ }
1453
+ return;
1454
+ }
1455
+ if (require_metrics.BYTEPLUS_VIDEO_SUBMIT_RE.test(pathname) && req.method === "POST") {
1456
+ setCorsHeaders(res);
1457
+ try {
1458
+ await require_byteplus_video.handleBytePlusVideoCreate(req, res, await require_helpers.readBody(req), fixtures, journal, defaults, setCorsHeaders, bytePlusVideoJobs);
1459
+ } catch (err) {
1460
+ const msg = err instanceof Error ? err.message : "Internal error";
1461
+ defaults.logger.error(`byteplus-video submit: ${msg}`);
1462
+ if (!res.headersSent) {
1463
+ try {
1464
+ journal.add({
1465
+ method: req.method ?? "POST",
1466
+ path: req.url ?? pathname,
1467
+ headers: require_helpers.flattenHeaders(req.headers),
1468
+ body: null,
1469
+ response: {
1470
+ status: 500,
1471
+ fixture: null
1472
+ }
1473
+ });
1474
+ } catch (jErr) {
1475
+ defaults.logger.warn(`byteplus-video submit: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
1476
+ }
1477
+ require_sse_writer.writeErrorResponse(res, 500, JSON.stringify({ error: {
1478
+ message: msg,
1479
+ type: "server_error"
1480
+ } }));
1481
+ } else if (!res.writableEnded) res.destroy();
1482
+ }
1483
+ return;
1484
+ }
1098
1485
  if (pathname === "/api/v1/models" && req.method === "GET") {
1099
1486
  require_openrouter_chat.handleOpenRouterModels(req, res, fixtures, journal, defaults, setCorsHeaders);
1100
1487
  return;
@@ -1293,7 +1680,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1293
1680
  }
1294
1681
  if (pathname === IMAGES_PATH && req.method === "POST") {
1295
1682
  try {
1296
- await require_images.handleImages(req, res, await require_helpers.readBody(req), fixtures, journal, defaults, setCorsHeaders);
1683
+ await require_images.handleImages(req, res, await require_helpers.readBody(req), fixtures, journal, defaults, setCorsHeaders, "openai", void 0, isBytePlusArk ? "byteplus" : void 0);
1297
1684
  } catch (err) {
1298
1685
  const msg = err instanceof Error ? err.message : "Internal error";
1299
1686
  if (!res.headersSent) require_sse_writer.writeErrorResponse(res, 500, JSON.stringify({ error: {
@@ -1744,7 +2131,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1744
2131
  try {
1745
2132
  falBody = req.method === "POST" || req.method === "PUT" ? await require_helpers.readBody(req) : "";
1746
2133
  const raw = falBody;
1747
- const chaosAction = require_chaos.evaluateChaos(null, defaults.chaos, req.headers, defaults.logger);
2134
+ const chaosAction = require_chaos.evaluateChaos(null, defaults.chaos, req.headers, defaults.logger, req.url);
1748
2135
  if (chaosAction) {
1749
2136
  require_chaos.applyChaosAction(chaosAction, res, null, journal, {
1750
2137
  method: req.method ?? "GET",
@@ -1786,7 +2173,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1786
2173
  setCorsHeaders(res);
1787
2174
  try {
1788
2175
  const raw = req.method === "POST" || req.method === "PUT" ? falBody ?? await require_helpers.readBody(req) : "{}";
1789
- const chaosAction = require_chaos.evaluateChaos(null, defaults.chaos, req.headers, defaults.logger);
2176
+ const chaosAction = require_chaos.evaluateChaos(null, defaults.chaos, req.headers, defaults.logger, req.url);
1790
2177
  if (chaosAction) {
1791
2178
  require_chaos.applyChaosAction(chaosAction, res, null, journal, {
1792
2179
  method: req.method ?? "GET",
@@ -1832,7 +2219,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1832
2219
  handleNotFound(res, "Not found");
1833
2220
  return;
1834
2221
  }
1835
- const completionsProvider = azureDeploymentId ? "azure" : isOpenRouter ? "openrouter" : "openai";
2222
+ const completionsProvider = azureDeploymentId ? "azure" : isOpenRouter ? "openrouter" : isBytePlusArk ? "byteplus" : "openai";
1836
2223
  try {
1837
2224
  await handleCompletions(req, res, fixtures, journal, defaults, azureDeploymentId, completionsProvider, isOpenRouter);
1838
2225
  } catch (err) {
@@ -1970,6 +2357,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1970
2357
  openRouterVideoJobs.clear();
1971
2358
  veoVideoJobs.clear();
1972
2359
  grokVideoJobs.clear();
2360
+ bytePlusVideoJobs.clear();
1973
2361
  originalClose(callback);
1974
2362
  return this;
1975
2363
  };
@@ -1993,7 +2381,8 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1993
2381
  videoStates,
1994
2382
  openRouterVideoJobs,
1995
2383
  veoVideoJobs,
1996
- grokVideoJobs
2384
+ grokVideoJobs,
2385
+ bytePlusVideoJobs
1997
2386
  });
1998
2387
  });
1999
2388
  });