@copilotkit/aimock 1.41.0 → 1.42.1

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 (218) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +18 -1
  4. package/README.md +2 -1
  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.ts.map +1 -1
  14. package/dist/agui-mock.cjs +2 -0
  15. package/dist/agui-mock.cjs.map +1 -1
  16. package/dist/agui-mock.d.cts.map +1 -1
  17. package/dist/agui-mock.d.ts.map +1 -1
  18. package/dist/agui-mock.js +2 -0
  19. package/dist/agui-mock.js.map +1 -1
  20. package/dist/bedrock-converse.cjs +36 -2
  21. package/dist/bedrock-converse.cjs.map +1 -1
  22. package/dist/bedrock-converse.d.cts.map +1 -1
  23. package/dist/bedrock-converse.d.ts.map +1 -1
  24. package/dist/bedrock-converse.js +37 -3
  25. package/dist/bedrock-converse.js.map +1 -1
  26. package/dist/bedrock.cjs +50 -13
  27. package/dist/bedrock.cjs.map +1 -1
  28. package/dist/bedrock.d.cts.map +1 -1
  29. package/dist/bedrock.d.ts.map +1 -1
  30. package/dist/bedrock.js +51 -14
  31. package/dist/bedrock.js.map +1 -1
  32. package/dist/byteplus-video.cjs +2 -2
  33. package/dist/byteplus-video.cjs.map +1 -1
  34. package/dist/byteplus-video.d.cts.map +1 -1
  35. package/dist/byteplus-video.d.ts.map +1 -1
  36. package/dist/byteplus-video.js +2 -2
  37. package/dist/byteplus-video.js.map +1 -1
  38. package/dist/chaos.cjs +36 -7
  39. package/dist/chaos.cjs.map +1 -1
  40. package/dist/chaos.d.cts +8 -3
  41. package/dist/chaos.d.cts.map +1 -1
  42. package/dist/chaos.d.ts +8 -3
  43. package/dist/chaos.d.ts.map +1 -1
  44. package/dist/chaos.js +36 -8
  45. package/dist/chaos.js.map +1 -1
  46. package/dist/cohere.cjs +58 -5
  47. package/dist/cohere.cjs.map +1 -1
  48. package/dist/cohere.d.cts.map +1 -1
  49. package/dist/cohere.d.ts.map +1 -1
  50. package/dist/cohere.js +59 -6
  51. package/dist/cohere.js.map +1 -1
  52. package/dist/config-loader.d.cts.map +1 -1
  53. package/dist/config-loader.d.ts.map +1 -1
  54. package/dist/elevenlabs-audio.cjs +36 -2
  55. package/dist/elevenlabs-audio.cjs.map +1 -1
  56. package/dist/elevenlabs-audio.d.cts.map +1 -1
  57. package/dist/elevenlabs-audio.d.ts.map +1 -1
  58. package/dist/elevenlabs-audio.js +37 -3
  59. package/dist/elevenlabs-audio.js.map +1 -1
  60. package/dist/embeddings.cjs +58 -3
  61. package/dist/embeddings.cjs.map +1 -1
  62. package/dist/embeddings.d.cts.map +1 -1
  63. package/dist/embeddings.d.ts.map +1 -1
  64. package/dist/embeddings.js +59 -4
  65. package/dist/embeddings.js.map +1 -1
  66. package/dist/fal-audio.cjs +38 -2
  67. package/dist/fal-audio.cjs.map +1 -1
  68. package/dist/fal-audio.d.cts.map +1 -1
  69. package/dist/fal-audio.d.ts.map +1 -1
  70. package/dist/fal-audio.js +39 -3
  71. package/dist/fal-audio.js.map +1 -1
  72. package/dist/fixture-loader.cjs +0 -0
  73. package/dist/fixture-loader.cjs.map +1 -1
  74. package/dist/fixture-loader.d.cts +9 -1
  75. package/dist/fixture-loader.d.cts.map +1 -1
  76. package/dist/fixture-loader.d.ts +9 -1
  77. package/dist/fixture-loader.d.ts.map +1 -1
  78. package/dist/fixture-loader.js +0 -0
  79. package/dist/fixture-loader.js.map +1 -1
  80. package/dist/gemini-embeddings.cjs +19 -1
  81. package/dist/gemini-embeddings.cjs.map +1 -1
  82. package/dist/gemini-embeddings.js +20 -2
  83. package/dist/gemini-embeddings.js.map +1 -1
  84. package/dist/gemini-interactions.cjs +15 -1
  85. package/dist/gemini-interactions.cjs.map +1 -1
  86. package/dist/gemini-interactions.d.cts.map +1 -1
  87. package/dist/gemini-interactions.d.ts.map +1 -1
  88. package/dist/gemini-interactions.js +16 -2
  89. package/dist/gemini-interactions.js.map +1 -1
  90. package/dist/gemini.cjs +70 -6
  91. package/dist/gemini.cjs.map +1 -1
  92. package/dist/gemini.d.cts.map +1 -1
  93. package/dist/gemini.d.ts.map +1 -1
  94. package/dist/gemini.js +71 -7
  95. package/dist/gemini.js.map +1 -1
  96. package/dist/grok-video.cjs +2 -2
  97. package/dist/grok-video.cjs.map +1 -1
  98. package/dist/grok-video.d.cts.map +1 -1
  99. package/dist/grok-video.d.ts.map +1 -1
  100. package/dist/grok-video.js +2 -2
  101. package/dist/grok-video.js.map +1 -1
  102. package/dist/helpers.cjs +168 -7
  103. package/dist/helpers.cjs.map +1 -1
  104. package/dist/helpers.d.cts.map +1 -1
  105. package/dist/helpers.d.ts.map +1 -1
  106. package/dist/helpers.js +160 -8
  107. package/dist/helpers.js.map +1 -1
  108. package/dist/images.cjs +22 -4
  109. package/dist/images.cjs.map +1 -1
  110. package/dist/images.d.cts.map +1 -1
  111. package/dist/images.d.ts.map +1 -1
  112. package/dist/images.js +23 -5
  113. package/dist/images.js.map +1 -1
  114. package/dist/llmock.cjs +15 -0
  115. package/dist/llmock.cjs.map +1 -1
  116. package/dist/llmock.d.cts +12 -0
  117. package/dist/llmock.d.cts.map +1 -1
  118. package/dist/llmock.d.ts +12 -0
  119. package/dist/llmock.d.ts.map +1 -1
  120. package/dist/llmock.js +16 -1
  121. package/dist/llmock.js.map +1 -1
  122. package/dist/mcp-mock.cjs +11 -0
  123. package/dist/mcp-mock.cjs.map +1 -1
  124. package/dist/mcp-mock.d.cts.map +1 -1
  125. package/dist/mcp-mock.d.ts.map +1 -1
  126. package/dist/mcp-mock.js +11 -0
  127. package/dist/mcp-mock.js.map +1 -1
  128. package/dist/messages.cjs +18 -1
  129. package/dist/messages.cjs.map +1 -1
  130. package/dist/messages.d.cts.map +1 -1
  131. package/dist/messages.d.ts.map +1 -1
  132. package/dist/messages.js +19 -2
  133. package/dist/messages.js.map +1 -1
  134. package/dist/moderation.cjs +67 -3
  135. package/dist/moderation.cjs.map +1 -1
  136. package/dist/moderation.d.cts +1 -0
  137. package/dist/moderation.d.cts.map +1 -1
  138. package/dist/moderation.d.ts +1 -0
  139. package/dist/moderation.d.ts.map +1 -1
  140. package/dist/moderation.js +68 -4
  141. package/dist/moderation.js.map +1 -1
  142. package/dist/ollama.cjs +75 -5
  143. package/dist/ollama.cjs.map +1 -1
  144. package/dist/ollama.d.cts.map +1 -1
  145. package/dist/ollama.d.ts.map +1 -1
  146. package/dist/ollama.js +76 -6
  147. package/dist/ollama.js.map +1 -1
  148. package/dist/openrouter-video.cjs +4 -4
  149. package/dist/openrouter-video.cjs.map +1 -1
  150. package/dist/openrouter-video.d.cts.map +1 -1
  151. package/dist/openrouter-video.d.ts.map +1 -1
  152. package/dist/openrouter-video.js +4 -4
  153. package/dist/openrouter-video.js.map +1 -1
  154. package/dist/rerank.cjs +65 -2
  155. package/dist/rerank.cjs.map +1 -1
  156. package/dist/rerank.d.cts +1 -1
  157. package/dist/rerank.d.cts.map +1 -1
  158. package/dist/rerank.d.ts +1 -1
  159. package/dist/rerank.d.ts.map +1 -1
  160. package/dist/rerank.js +66 -3
  161. package/dist/rerank.js.map +1 -1
  162. package/dist/responses.cjs +18 -1
  163. package/dist/responses.cjs.map +1 -1
  164. package/dist/responses.d.cts.map +1 -1
  165. package/dist/responses.d.ts.map +1 -1
  166. package/dist/responses.js +19 -2
  167. package/dist/responses.js.map +1 -1
  168. package/dist/search.cjs +67 -3
  169. package/dist/search.cjs.map +1 -1
  170. package/dist/search.d.cts +1 -0
  171. package/dist/search.d.cts.map +1 -1
  172. package/dist/search.d.ts +1 -0
  173. package/dist/search.d.ts.map +1 -1
  174. package/dist/search.js +68 -4
  175. package/dist/search.js.map +1 -1
  176. package/dist/server.cjs +317 -13
  177. package/dist/server.cjs.map +1 -1
  178. package/dist/server.d.cts.map +1 -1
  179. package/dist/server.d.ts.map +1 -1
  180. package/dist/server.js +320 -16
  181. package/dist/server.js.map +1 -1
  182. package/dist/speech.cjs +18 -1
  183. package/dist/speech.cjs.map +1 -1
  184. package/dist/speech.d.cts.map +1 -1
  185. package/dist/speech.d.ts.map +1 -1
  186. package/dist/speech.js +19 -2
  187. package/dist/speech.js.map +1 -1
  188. package/dist/transcription.cjs +21 -2
  189. package/dist/transcription.cjs.map +1 -1
  190. package/dist/transcription.d.cts.map +1 -1
  191. package/dist/transcription.d.ts.map +1 -1
  192. package/dist/transcription.js +21 -2
  193. package/dist/transcription.js.map +1 -1
  194. package/dist/types.d.cts +39 -2
  195. package/dist/types.d.cts.map +1 -1
  196. package/dist/types.d.ts +39 -2
  197. package/dist/types.d.ts.map +1 -1
  198. package/dist/vector-mock.cjs +88 -0
  199. package/dist/vector-mock.cjs.map +1 -1
  200. package/dist/vector-mock.d.cts.map +1 -1
  201. package/dist/vector-mock.d.ts.map +1 -1
  202. package/dist/vector-mock.js +89 -1
  203. package/dist/vector-mock.js.map +1 -1
  204. package/dist/vector-types.d.cts.map +1 -1
  205. package/dist/vector-types.d.ts.map +1 -1
  206. package/dist/veo-video.cjs +2 -2
  207. package/dist/veo-video.cjs.map +1 -1
  208. package/dist/veo-video.d.cts.map +1 -1
  209. package/dist/veo-video.d.ts.map +1 -1
  210. package/dist/veo-video.js +2 -2
  211. package/dist/veo-video.js.map +1 -1
  212. package/dist/video.cjs +19 -2
  213. package/dist/video.cjs.map +1 -1
  214. package/dist/video.d.cts.map +1 -1
  215. package/dist/video.d.ts.map +1 -1
  216. package/dist/video.js +20 -3
  217. package/dist/video.js.map +1 -1
  218. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.cts","names":[],"sources":["../src/server.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;UAgIiB,cAAA;EAAA,MAAA,EACP,MAAA,CAAK,MADgB;EAAA,OAAA,EAEpB,OAFoB;KACrB,EAAA,MAAK;UACJ,EAEC,eAFD;aAEC,EACG,aADH;qBACG,EACQ,qBADR;cACQ,EACP,cADO;eACP,EACC,eADD;mBACC,EACI,mBADJ;;;AA+rCjB;;;;;UAAiB,eAAA;UACP;UACA;cACI;;iBAMQ,YAAA,WACV,qBACA,4BACD;;WAA+B;sBACtB,kBACjB,QAAQ"}
1
+ {"version":3,"file":"server.d.cts","names":[],"sources":["../src/server.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;UAgJiB,cAAA;EAAA,MAAA,EACP,MAAA,CAAK,MADgB;EAAA,OAAA,EAEpB,OAFoB;KACrB,EAAA,MAAK;UACJ,EAEC,eAFD;aAEC,EACG,aADH;qBACG,EACQ,qBADR;cACQ,EACP,cADO;eACP,EACC,eADD;mBACC,EACI,mBADJ;;;AAqkDjB;;;;;UAAiB,eAAA;UACP;UACA;cACI;;iBAMQ,YAAA,WACV,qBACA,4BACD;;WAA+B;sBACtB,kBACjB,QAAQ"}
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","names":[],"sources":["../src/server.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;UAgIiB,cAAA;EAAA,MAAA,EACP,MAAA,CAAK,MADgB;EAAA,OAAA,EAEpB,OAFoB;KACrB,EAAA,MAAK;UACJ,EAEC,eAFD;aAEC,EACG,aADH;qBACG,EACQ,qBADR;cACQ,EACP,cADO;eACP,EACC,eADD;mBACC,EACI,mBADJ;;;AA+rCjB;;;;;UAAiB,eAAA;UACP;UACA;cACI;;iBAMQ,YAAA,WACV,qBACA,4BACD;;WAA+B;sBACtB,kBACjB,QAAQ"}
1
+ {"version":3,"file":"server.d.ts","names":[],"sources":["../src/server.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;UAgJiB,cAAA;EAAA,MAAA,EACP,MAAA,CAAK,MADgB;EAAA,OAAA,EAEpB,OAFoB;KACrB,EAAA,MAAK;UACJ,EAEC,eAFD;aAEC,EACG,aADH;qBACG,EACQ,qBADR;cACQ,EACP,cADO;eACP,EACC,eADD;mBACC,EACI,mBADJ;;;AAqkDjB;;;;;UAAiB,eAAA;UACP;UACA;cACI;;iBAMQ,YAAA,WACV,qBACA,4BACD;;WAA+B;sBACtB,kBACjB,QAAQ"}
package/dist/server.js CHANGED
@@ -1,13 +1,14 @@
1
+ import { DEFAULT_TEST_ID } from "./constants.js";
1
2
  import { markAuthenticatedRequest, resolveInboundAuth, validateRequestApiKey, writeApiKeyHttpRejection, writeApiKeyUpgradeRejection } from "./api-key-auth.js";
2
- import { buildContentWithToolCallsChunks, buildContentWithToolCallsCompletion, buildTextChunks, buildTextCompletion, buildToolCallChunks, buildToolCallCompletion, buildUsageChunk, extractOverrides, flattenHeaders, getContext, getTestId, isAudioResponse, isContentWithToolCallsResponse, isErrorResponse, isTextResponse, isToolCallResponse, readBody, resolveFixtureBlocks, resolveReasoningForModel, resolveResponse, resolveStrictMode, resolveUsage, serializeErrorResponse, strictNoMatchLogLine, strictNoMatchMessage, strictOverrideField } from "./helpers.js";
3
+ import { buildContentWithToolCallsChunks, buildContentWithToolCallsCompletion, buildTextChunks, buildTextCompletion, buildToolCallChunks, buildToolCallCompletion, buildUsageChunk, extractOverrides, flattenHeaders, getContext, getTestId, isAudioResponse, isContentWithToolCallsResponse, isEmbeddingResponse, isErrorResponse, isImageResponse, isJSONResponse, isJsonObject, isResponseFactory, isTextResponse, isToolCallResponse, isTranscriptionResponse, isVideoResponse, readBody, resolveFixtureBlocks, resolveReasoningForModel, resolveResponse, resolveStrictMode, resolveUsage, serializeErrorResponse, strictNoMatchLogLine, strictNoMatchMessage, strictOverrideField } from "./helpers.js";
3
4
  import { Logger } from "./logger.js";
4
5
  import { Journal } from "./journal.js";
5
6
  import { matchFixtureDiagnostic, recordMatchOptions } from "./router.js";
6
- import { entryToFixture, validateFixtures } from "./fixture-loader.js";
7
+ import { INJECTED_STATUS_RANGE, entryToFixture, isInjectableStatus, validateFixtures } from "./fixture-loader.js";
7
8
  import { writeErrorResponse, writeSSEStream } from "./sse-writer.js";
8
9
  import { createInterruptionSignal } from "./interruption.js";
9
10
  import { buildOpenRouterCandidates, handleOpenRouterCredits, handleOpenRouterKey, handleOpenRouterModels, isOpenRouterPath, resolveOpenRouterShaping, serializeOpenRouterError, shapeOpenRouterChunks, shapeOpenRouterCompletion } from "./openrouter-chat.js";
10
- import { applyChaosAction, evaluateChaos } from "./chaos.js";
11
+ import { applyChaosAction, evaluateChaos, isChaosScope } from "./chaos.js";
11
12
  import { proxyAndRecord } from "./recorder.js";
12
13
  import { handleResponses } from "./responses.js";
13
14
  import { handleMessages } from "./messages.js";
@@ -125,7 +126,8 @@ const DEFAULT_MODELS = [
125
126
  const CORS_HEADERS = {
126
127
  "Access-Control-Allow-Origin": "*",
127
128
  "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
128
- "Access-Control-Allow-Headers": "*"
129
+ "Access-Control-Allow-Headers": "*",
130
+ "Access-Control-Expose-Headers": "X-Total-Count"
129
131
  };
130
132
  function setCorsHeaders(res) {
131
133
  for (const [key, value] of Object.entries(CORS_HEADERS)) res.setHeader(key, value);
@@ -143,11 +145,24 @@ function handleNotFound(res, message) {
143
145
  } }));
144
146
  }
145
147
  const CONTROL_PREFIX = "/__aimock";
148
+ /** The complete `GET /__aimock/fixtures` query-param vocabulary; anything else 400s. */
149
+ const FIXTURES_PARAMS = new Set(["include"]);
150
+ /** The complete `GET /__aimock/journal` query-param vocabulary; anything else 400s. */
151
+ const JOURNAL_PARAMS = new Set([
152
+ "limit",
153
+ "offset",
154
+ "path",
155
+ "method",
156
+ "status",
157
+ "service",
158
+ "testId"
159
+ ]);
146
160
  /**
147
161
  * Perform a full reset: clear the fixtures array, the journal (entries *and*
148
162
  * per-test fixture match-counts, i.e. sequence position), the video and fal.ai
149
- * job/queue state, and the Gemini interaction/event-id counters, then re-zero
150
- * the `aimock_fixtures_loaded` gauge.
163
+ * job/queue state, the Gemini interaction/event-id counters, and any runtime
164
+ * chaos override (reverting to the construction-time chaos config), then
165
+ * re-zero the `aimock_fixtures_loaded` gauge.
151
166
  *
152
167
  * `targets` is `null` when no server is running (an in-process `reset()` before
153
168
  * `start()`). The process-global generation state is reset either way, since it
@@ -164,6 +179,7 @@ function performFullReset(fixtures, targets) {
164
179
  resetEventIdCounter();
165
180
  if (!targets) return;
166
181
  targets.journal.clear();
182
+ targets.defaults.chaos = void 0;
167
183
  targets.videoStates.clear();
168
184
  targets.openRouterVideoJobs.clear();
169
185
  targets.veoVideoJobs.clear();
@@ -172,10 +188,103 @@ function performFullReset(fixtures, targets) {
172
188
  if (targets.defaults.registry) targets.defaults.registry.setGauge("aimock_fixtures_loaded", {}, fixtures.length);
173
189
  }
174
190
  /**
191
+ * JSON-safe redaction for control-API inspection output. Functions (fixture
192
+ * `predicate`s, response factories) and RegExps do not survive
193
+ * JSON.stringify as anything useful, so they become marker strings.
194
+ */
195
+ function redactForInspection(value) {
196
+ if (typeof value === "function") return "[function]";
197
+ if (value instanceof RegExp) return String(value);
198
+ if (Array.isArray(value)) return value.map(redactForInspection);
199
+ if (value !== null && typeof value === "object") {
200
+ const out = {};
201
+ for (const [key, entry] of Object.entries(value)) out[key] = redactForInspection(entry);
202
+ return out;
203
+ }
204
+ return value;
205
+ }
206
+ /**
207
+ * One-line kind label for a fixture response used by
208
+ * `GET /__aimock/fixtures?include=fixtures` (e.g. "text", "error").
209
+ * Response factories are reported as "factory".
210
+ *
211
+ * Discriminates with the same `is*Response` guards the request handlers use,
212
+ * in the same order — NOT by inspecting keys. Every response shape extends
213
+ * `ResponseOverrides` (`id`/`created`/`model`/`usage`), so a recorded fixture's
214
+ * first key is usually an envelope override rather than the discriminant. The
215
+ * audio-before-content/toolCalls order is the ORDERING CONTRACT documented on
216
+ * `AudioResponse` in types.ts: those shapes structurally overlap.
217
+ */
218
+ function fixtureResponseKind(response) {
219
+ if (isResponseFactory(response)) return "factory";
220
+ if (isErrorResponse(response)) return "error";
221
+ if (isAudioResponse(response)) return "audio";
222
+ if (isVideoResponse(response)) return "video";
223
+ if (isTranscriptionResponse(response)) return "transcription";
224
+ if (isImageResponse(response)) return "image";
225
+ if (isEmbeddingResponse(response)) return "embedding";
226
+ if (isJSONResponse(response)) return "json";
227
+ if (isContentWithToolCallsResponse(response)) return "contentWithToolCalls";
228
+ if (isToolCallResponse(response)) return "toolCalls";
229
+ if (isTextResponse(response)) return "text";
230
+ return "unknown";
231
+ }
232
+ /**
233
+ * Resolve a journal entry's testId exactly the way `getTestId` resolves an
234
+ * incoming request's: the `X-Test-Id` header wins, then `?testId=` parsed out
235
+ * of the query string, then `DEFAULT_TEST_ID`. The
236
+ * filter and the server's own per-test isolation must never disagree — and a
237
+ * raw `path.includes("testId=t1")` both prefix-collides with `t10` and matches
238
+ * unrelated params like `notTestId`.
239
+ */
240
+ function journalEntryTestId(entry) {
241
+ const header = entry.headers["x-test-id"];
242
+ if (header) return header;
243
+ const qIdx = entry.path.indexOf("?");
244
+ if (qIdx !== -1) {
245
+ const queryValue = new URLSearchParams(entry.path.slice(qIdx + 1)).get("testId");
246
+ if (queryValue) return queryValue;
247
+ }
248
+ return DEFAULT_TEST_ID;
249
+ }
250
+ /**
251
+ * The testId a chaos override is stored under, and read back with — resolved by
252
+ * `getTestId`, i.e. `X-Test-Id` header then `?testId=`, exactly as
253
+ * `resolveScopedDefaults` resolves the traffic being evaluated. A control call
254
+ * and the requests it is meant to affect can then never land in different
255
+ * scopes. `DEFAULT_TEST_ID` (neither header nor param) means the server-wide
256
+ * baseline.
257
+ *
258
+ * Returns `null` when `X-Test-Id` is PRESENT but blank. `String(testId ?? "")`
259
+ * in a harness produces that trivially, and silently treating it as "untagged"
260
+ * would install a server-wide baseline that fails every other test — the exact
261
+ * cross-test leak per-testId scoping exists to prevent. Callers must 400.
262
+ */
263
+ function chaosScopeId(req) {
264
+ const headerValue = req.headers["x-test-id"];
265
+ const raw = Array.isArray(headerValue) ? headerValue[0] : headerValue;
266
+ if (typeof raw === "string" && raw.trim() === "") return null;
267
+ return getTestId(req);
268
+ }
269
+ /** 400 for a control call whose `X-Test-Id` is present but blank. */
270
+ function writeBlankTestId(res) {
271
+ res.writeHead(400, { "Content-Type": "application/json" });
272
+ res.end(JSON.stringify({ error: "Invalid 'X-Test-Id': header is present but empty" }));
273
+ return true;
274
+ }
275
+ /** The chaos config in effect for one testId: its override, else the baseline. */
276
+ function effectiveChaos(defaults, scopeId) {
277
+ const scoped = defaults.chaosByTestId?.get(scopeId);
278
+ if (scoped) return scoped;
279
+ const current = defaults.chaos;
280
+ if (!current) return {};
281
+ return isChaosScope(current) ? current.base ?? {} : current;
282
+ }
283
+ /**
175
284
  * Handle requests under `/__aimock/`. Returns `true` if the request was
176
285
  * handled, `false` if the path doesn't match the control prefix.
177
286
  */
178
- async function handleControlAPI(req, res, pathname, fixtures, journal, videoStates, openRouterVideoJobs, veoVideoJobs, grokVideoJobs, bytePlusVideoJobs, defaults) {
287
+ async function handleControlAPI(req, res, pathname, searchParams, fixtures, journal, videoStates, openRouterVideoJobs, veoVideoJobs, grokVideoJobs, bytePlusVideoJobs, defaults) {
179
288
  if (!pathname.startsWith(CONTROL_PREFIX)) return false;
180
289
  const subPath = pathname.slice(9);
181
290
  setCorsHeaders(res);
@@ -185,13 +294,148 @@ async function handleControlAPI(req, res, pathname, fixtures, journal, videoStat
185
294
  return true;
186
295
  }
187
296
  if (subPath === "/journal" && req.method === "GET") {
188
- res.writeHead(200, { "Content-Type": "application/json" });
189
- res.end(JSON.stringify(journal.getAll()));
297
+ const bad = (message) => {
298
+ res.writeHead(400, { "Content-Type": "application/json" });
299
+ res.end(JSON.stringify({ error: message }));
300
+ return true;
301
+ };
302
+ for (const key of searchParams.keys()) if (!JOURNAL_PARAMS.has(key)) return bad(`Unknown query parameter: '${key}'. Supported: ${[...JOURNAL_PARAMS].join(", ")}`);
303
+ const parseNonNegativeInt = (name, fallback) => {
304
+ const raw = searchParams.get(name);
305
+ if (raw === null) return fallback;
306
+ if (!/^\d+$/.test(raw.trim())) return null;
307
+ return Number(raw);
308
+ };
309
+ const limit = parseNonNegativeInt("limit", -1);
310
+ const offset = parseNonNegativeInt("offset", 0);
311
+ if (limit === null) return bad("Invalid 'limit': must be an integer >= 0");
312
+ if (offset === null) return bad("Invalid 'offset': must be an integer >= 0");
313
+ let statusFilter;
314
+ const statusRaw = searchParams.get("status");
315
+ if (statusRaw !== null) {
316
+ if (!/^\d+$/.test(statusRaw.trim())) return bad("Invalid 'status': must be an integer");
317
+ statusFilter = Number(statusRaw);
318
+ }
319
+ const pathFilter = searchParams.get("path");
320
+ const methodFilter = searchParams.get("method");
321
+ const serviceFilter = searchParams.get("service");
322
+ const testIdFilter = searchParams.get("testId");
323
+ let entries = journal.getAll();
324
+ if (methodFilter !== null) {
325
+ const want = methodFilter.toUpperCase();
326
+ entries = entries.filter((e) => e.method.toUpperCase() === want);
327
+ }
328
+ if (pathFilter !== null) entries = entries.filter((e) => e.path.includes(pathFilter));
329
+ if (statusFilter !== void 0) entries = entries.filter((e) => e.response.status === statusFilter);
330
+ if (serviceFilter !== null) entries = entries.filter((e) => e.service === serviceFilter);
331
+ if (testIdFilter !== null) entries = entries.filter((e) => journalEntryTestId(e) === testIdFilter);
332
+ const total = entries.length;
333
+ if (offset > 0) entries = entries.slice(offset);
334
+ if (limit >= 0) entries = entries.slice(0, limit);
335
+ res.writeHead(200, {
336
+ "Content-Type": "application/json",
337
+ "X-Total-Count": String(total)
338
+ });
339
+ res.end(JSON.stringify(entries));
190
340
  return true;
191
341
  }
192
342
  if (subPath === "/fixtures" && req.method === "GET") {
343
+ for (const key of searchParams.keys()) if (!FIXTURES_PARAMS.has(key)) {
344
+ res.writeHead(400, { "Content-Type": "application/json" });
345
+ res.end(JSON.stringify({ error: `Unknown query parameter: '${key}'. Supported: ${[...FIXTURES_PARAMS].join(", ")}` }));
346
+ return true;
347
+ }
348
+ const include = searchParams.get("include");
349
+ if (include === null) {
350
+ res.writeHead(200, { "Content-Type": "application/json" });
351
+ res.end(JSON.stringify({ count: fixtures.length }));
352
+ return true;
353
+ }
354
+ if (include !== "fixtures") {
355
+ res.writeHead(400, { "Content-Type": "application/json" });
356
+ res.end(JSON.stringify({ error: "Invalid 'include': expected 'fixtures'" }));
357
+ return true;
358
+ }
359
+ res.writeHead(200, { "Content-Type": "application/json" });
360
+ res.end(JSON.stringify({
361
+ count: fixtures.length,
362
+ fixtures: fixtures.map((fixture, index) => ({
363
+ index,
364
+ match: redactForInspection(fixture.match),
365
+ responseKind: fixtureResponseKind(fixture.response),
366
+ ...fixture.latency !== void 0 ? { latency: fixture.latency } : {},
367
+ ...fixture.chaos !== void 0 ? { chaos: fixture.chaos } : {}
368
+ }))
369
+ }));
370
+ return true;
371
+ }
372
+ if (subPath === "/chaos" && req.method === "GET") {
373
+ const scopeId = chaosScopeId(req);
374
+ if (scopeId === null) return writeBlankTestId(res);
375
+ res.writeHead(200, { "Content-Type": "application/json" });
376
+ res.end(JSON.stringify({ chaos: effectiveChaos(defaults, scopeId) }));
377
+ return true;
378
+ }
379
+ if (subPath === "/chaos" && req.method === "DELETE") {
380
+ const scopeId = chaosScopeId(req);
381
+ if (scopeId === null) return writeBlankTestId(res);
382
+ defaults.chaosByTestId?.delete(scopeId);
383
+ res.writeHead(200, { "Content-Type": "application/json" });
384
+ res.end(JSON.stringify({ chaos: effectiveChaos(defaults, scopeId) }));
385
+ return true;
386
+ }
387
+ if (subPath === "/chaos" && req.method === "POST") {
388
+ const scopeId = chaosScopeId(req);
389
+ if (scopeId === null) return writeBlankTestId(res);
390
+ let raw;
391
+ try {
392
+ raw = await readBody(req);
393
+ } catch (err) {
394
+ const msg = err instanceof Error ? err.message : String(err);
395
+ defaults.logger.error(`POST /__aimock/chaos: failed to read body: ${msg}`);
396
+ res.writeHead(400, { "Content-Type": "application/json" });
397
+ res.end(JSON.stringify({ error: `Failed to read request body: ${msg}` }));
398
+ return true;
399
+ }
400
+ let parsed;
401
+ try {
402
+ parsed = JSON.parse(raw);
403
+ } catch (err) {
404
+ const msg = err instanceof Error ? err.message : String(err);
405
+ defaults.logger.error(`POST /__aimock/chaos: invalid JSON: ${msg}`);
406
+ res.writeHead(400, { "Content-Type": "application/json" });
407
+ res.end(JSON.stringify({ error: `Invalid JSON: ${msg}` }));
408
+ return true;
409
+ }
410
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
411
+ res.writeHead(400, { "Content-Type": "application/json" });
412
+ res.end(JSON.stringify({ error: "Invalid body: expected a JSON object" }));
413
+ return true;
414
+ }
415
+ const allowed = [
416
+ "dropRate",
417
+ "malformedRate",
418
+ "disconnectRate"
419
+ ];
420
+ for (const key of Object.keys(parsed)) if (!allowed.includes(key)) {
421
+ res.writeHead(400, { "Content-Type": "application/json" });
422
+ res.end(JSON.stringify({ error: `Unknown chaos field: '${key}'` }));
423
+ return true;
424
+ }
425
+ const next = {};
426
+ for (const key of allowed) {
427
+ const value = parsed[key];
428
+ if (value === void 0) continue;
429
+ if (typeof value !== "number" || Number.isNaN(value) || value < 0 || value > 1) {
430
+ res.writeHead(400, { "Content-Type": "application/json" });
431
+ res.end(JSON.stringify({ error: `Invalid '${key}': must be a number between 0 and 1` }));
432
+ return true;
433
+ }
434
+ next[key] = value;
435
+ }
436
+ defaults.chaosByTestId?.set(scopeId, next);
193
437
  res.writeHead(200, { "Content-Type": "application/json" });
194
- res.end(JSON.stringify({ count: fixtures.length }));
438
+ res.end(JSON.stringify({ chaos: effectiveChaos(defaults, scopeId) }));
195
439
  return true;
196
440
  }
197
441
  if (subPath === "/fixtures" && req.method === "POST") {
@@ -300,8 +544,37 @@ async function handleControlAPI(req, res, pathname, fixtures, journal, videoStat
300
544
  res.end(JSON.stringify({ error: `Invalid JSON: ${msg}` }));
301
545
  return true;
302
546
  }
547
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
548
+ res.writeHead(400, { "Content-Type": "application/json" });
549
+ res.end(JSON.stringify({ error: "Invalid body: expected a JSON object" }));
550
+ return true;
551
+ }
303
552
  const status = parsed.status ?? 500;
553
+ if (!isInjectableStatus(status)) {
554
+ res.writeHead(400, { "Content-Type": "application/json" });
555
+ res.end(JSON.stringify({ error: `Invalid 'status': must be ${INJECTED_STATUS_RANGE}` }));
556
+ return true;
557
+ }
304
558
  const errorBody = parsed.body;
559
+ if (errorBody !== void 0 && errorBody !== null) {
560
+ if (typeof errorBody !== "object" || Array.isArray(errorBody)) {
561
+ res.writeHead(400, { "Content-Type": "application/json" });
562
+ res.end(JSON.stringify({ error: "Invalid 'body': must be an object" }));
563
+ return true;
564
+ }
565
+ for (const field of [
566
+ "message",
567
+ "type",
568
+ "code"
569
+ ]) {
570
+ const value = errorBody[field];
571
+ if (value !== void 0 && value !== null && typeof value !== "string") {
572
+ res.writeHead(400, { "Content-Type": "application/json" });
573
+ res.end(JSON.stringify({ error: `Invalid 'body.${field}': must be a string` }));
574
+ return true;
575
+ }
576
+ }
577
+ }
305
578
  const errorFixture = {
306
579
  match: { predicate: () => true },
307
580
  response: {
@@ -361,7 +634,6 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
361
634
  let body;
362
635
  try {
363
636
  body = JSON.parse(raw);
364
- if (modelFallback && !body.model) body.model = modelFallback;
365
637
  } catch (parseErr) {
366
638
  const detail = parseErr instanceof Error ? parseErr.message : "unknown parse error";
367
639
  journal.add({
@@ -382,6 +654,24 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
382
654
  } }));
383
655
  return;
384
656
  }
657
+ if (!isJsonObject(body)) {
658
+ journal.add({
659
+ method: req.method ?? "POST",
660
+ path: req.url ?? COMPLETIONS_PATH,
661
+ headers: flattenHeaders(req.headers),
662
+ body: null,
663
+ response: {
664
+ status: 400,
665
+ fixture: null
666
+ }
667
+ });
668
+ writeErrorResponse(res, 400, openRouter ? serializeOpenRouterError(400, "Request body must be a JSON object") : JSON.stringify({ error: {
669
+ message: "Request body must be a JSON object",
670
+ type: "invalid_request_error"
671
+ } }));
672
+ return;
673
+ }
674
+ if (modelFallback && !body.model) body.model = modelFallback;
385
675
  if (!Array.isArray(body.messages)) {
386
676
  journal.add({
387
677
  method: req.method ?? "POST",
@@ -473,7 +763,7 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
473
763
  const snippet = typeof lastUserMsg?.content === "string" ? lastUserMsg.content.slice(0, 80) : "";
474
764
  defaults.logger.debug(`No fixture matched for request (model=${body.model ?? "?"}, msg="${snippet}")`);
475
765
  }
476
- const chaosAction = evaluateChaos(fixture, defaults.chaos, req.headers, defaults.logger);
766
+ const chaosAction = evaluateChaos(fixture, defaults.chaos, req.headers, defaults.logger, req.url);
477
767
  const chaosContext = {
478
768
  method,
479
769
  path,
@@ -770,13 +1060,27 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
770
1060
  const logger = new Logger(options?.logLevel ?? "silent");
771
1061
  const registry = options?.metrics ? createMetricsRegistry() : void 0;
772
1062
  const serverOptions = options ?? {};
1063
+ const chaosByTestId = /* @__PURE__ */ new Map();
773
1064
  const defaults = {
774
1065
  latency: serverOptions.latency ?? 0,
775
1066
  chunkSize: Math.max(1, serverOptions.chunkSize ?? DEFAULT_CHUNK_SIZE),
776
1067
  replaySpeed: serverOptions.replaySpeed ?? 1,
777
1068
  logger,
1069
+ chaosByTestId,
778
1070
  get chaos() {
779
- return serverOptions.chaos;
1071
+ return {
1072
+ base: chaosByTestId.get(DEFAULT_TEST_ID) ?? serverOptions.chaos,
1073
+ byTestId: chaosByTestId
1074
+ };
1075
+ },
1076
+ set chaos(value) {
1077
+ if (value === void 0) {
1078
+ chaosByTestId.clear();
1079
+ return;
1080
+ }
1081
+ const base = isChaosScope(value) ? value.base : value;
1082
+ if (base === void 0) chaosByTestId.delete(DEFAULT_TEST_ID);
1083
+ else chaosByTestId.set(DEFAULT_TEST_ID, base);
780
1084
  },
781
1085
  registry,
782
1086
  get record() {
@@ -933,7 +1237,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
933
1237
  markAuthenticatedRequest(req, resolvedAuth.policy);
934
1238
  }
935
1239
  if (pathname.startsWith(CONTROL_PREFIX)) {
936
- await handleControlAPI(req, res, pathname, fixtures, journal, videoStates, openRouterVideoJobs, veoVideoJobs, grokVideoJobs, bytePlusVideoJobs, defaults);
1240
+ await handleControlAPI(req, res, pathname, parsedUrl.searchParams, fixtures, journal, videoStates, openRouterVideoJobs, veoVideoJobs, grokVideoJobs, bytePlusVideoJobs, defaults);
937
1241
  return;
938
1242
  }
939
1243
  if (mounts) {
@@ -1825,7 +2129,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1825
2129
  try {
1826
2130
  falBody = req.method === "POST" || req.method === "PUT" ? await readBody(req) : "";
1827
2131
  const raw = falBody;
1828
- const chaosAction = evaluateChaos(null, defaults.chaos, req.headers, defaults.logger);
2132
+ const chaosAction = evaluateChaos(null, defaults.chaos, req.headers, defaults.logger, req.url);
1829
2133
  if (chaosAction) {
1830
2134
  applyChaosAction(chaosAction, res, null, journal, {
1831
2135
  method: req.method ?? "GET",
@@ -1867,7 +2171,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1867
2171
  setCorsHeaders(res);
1868
2172
  try {
1869
2173
  const raw = req.method === "POST" || req.method === "PUT" ? falBody ?? await readBody(req) : "{}";
1870
- const chaosAction = evaluateChaos(null, defaults.chaos, req.headers, defaults.logger);
2174
+ const chaosAction = evaluateChaos(null, defaults.chaos, req.headers, defaults.logger, req.url);
1871
2175
  if (chaosAction) {
1872
2176
  applyChaosAction(chaosAction, res, null, journal, {
1873
2177
  method: req.method ?? "GET",