@copilotkit/aimock 1.41.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 (215) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +12 -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.ts.map +1 -1
  53. package/dist/elevenlabs-audio.cjs +36 -2
  54. package/dist/elevenlabs-audio.cjs.map +1 -1
  55. package/dist/elevenlabs-audio.d.cts.map +1 -1
  56. package/dist/elevenlabs-audio.d.ts.map +1 -1
  57. package/dist/elevenlabs-audio.js +37 -3
  58. package/dist/elevenlabs-audio.js.map +1 -1
  59. package/dist/embeddings.cjs +58 -3
  60. package/dist/embeddings.cjs.map +1 -1
  61. package/dist/embeddings.d.cts.map +1 -1
  62. package/dist/embeddings.d.ts.map +1 -1
  63. package/dist/embeddings.js +59 -4
  64. package/dist/embeddings.js.map +1 -1
  65. package/dist/fal-audio.cjs +38 -2
  66. package/dist/fal-audio.cjs.map +1 -1
  67. package/dist/fal-audio.d.cts.map +1 -1
  68. package/dist/fal-audio.d.ts.map +1 -1
  69. package/dist/fal-audio.js +39 -3
  70. package/dist/fal-audio.js.map +1 -1
  71. package/dist/fixture-loader.cjs +0 -0
  72. package/dist/fixture-loader.cjs.map +1 -1
  73. package/dist/fixture-loader.d.cts +9 -1
  74. package/dist/fixture-loader.d.cts.map +1 -1
  75. package/dist/fixture-loader.d.ts +9 -1
  76. package/dist/fixture-loader.d.ts.map +1 -1
  77. package/dist/fixture-loader.js +0 -0
  78. package/dist/fixture-loader.js.map +1 -1
  79. package/dist/gemini-embeddings.cjs +19 -1
  80. package/dist/gemini-embeddings.cjs.map +1 -1
  81. package/dist/gemini-embeddings.js +20 -2
  82. package/dist/gemini-embeddings.js.map +1 -1
  83. package/dist/gemini-interactions.cjs +15 -1
  84. package/dist/gemini-interactions.cjs.map +1 -1
  85. package/dist/gemini-interactions.d.cts.map +1 -1
  86. package/dist/gemini-interactions.d.ts.map +1 -1
  87. package/dist/gemini-interactions.js +16 -2
  88. package/dist/gemini-interactions.js.map +1 -1
  89. package/dist/gemini.cjs +70 -6
  90. package/dist/gemini.cjs.map +1 -1
  91. package/dist/gemini.d.cts.map +1 -1
  92. package/dist/gemini.d.ts.map +1 -1
  93. package/dist/gemini.js +71 -7
  94. package/dist/gemini.js.map +1 -1
  95. package/dist/grok-video.cjs +2 -2
  96. package/dist/grok-video.cjs.map +1 -1
  97. package/dist/grok-video.d.cts.map +1 -1
  98. package/dist/grok-video.d.ts.map +1 -1
  99. package/dist/grok-video.js +2 -2
  100. package/dist/grok-video.js.map +1 -1
  101. package/dist/helpers.cjs +160 -5
  102. package/dist/helpers.cjs.map +1 -1
  103. package/dist/helpers.d.cts.map +1 -1
  104. package/dist/helpers.d.ts.map +1 -1
  105. package/dist/helpers.js +152 -6
  106. package/dist/helpers.js.map +1 -1
  107. package/dist/images.cjs +22 -4
  108. package/dist/images.cjs.map +1 -1
  109. package/dist/images.d.cts.map +1 -1
  110. package/dist/images.d.ts.map +1 -1
  111. package/dist/images.js +23 -5
  112. package/dist/images.js.map +1 -1
  113. package/dist/llmock.cjs +15 -0
  114. package/dist/llmock.cjs.map +1 -1
  115. package/dist/llmock.d.cts +12 -0
  116. package/dist/llmock.d.cts.map +1 -1
  117. package/dist/llmock.d.ts +12 -0
  118. package/dist/llmock.d.ts.map +1 -1
  119. package/dist/llmock.js +16 -1
  120. package/dist/llmock.js.map +1 -1
  121. package/dist/mcp-mock.cjs +11 -0
  122. package/dist/mcp-mock.cjs.map +1 -1
  123. package/dist/mcp-mock.d.cts.map +1 -1
  124. package/dist/mcp-mock.d.ts.map +1 -1
  125. package/dist/mcp-mock.js +11 -0
  126. package/dist/mcp-mock.js.map +1 -1
  127. package/dist/messages.cjs +18 -1
  128. package/dist/messages.cjs.map +1 -1
  129. package/dist/messages.d.cts.map +1 -1
  130. package/dist/messages.d.ts.map +1 -1
  131. package/dist/messages.js +19 -2
  132. package/dist/messages.js.map +1 -1
  133. package/dist/moderation.cjs +67 -3
  134. package/dist/moderation.cjs.map +1 -1
  135. package/dist/moderation.d.cts +1 -0
  136. package/dist/moderation.d.cts.map +1 -1
  137. package/dist/moderation.d.ts +1 -0
  138. package/dist/moderation.d.ts.map +1 -1
  139. package/dist/moderation.js +68 -4
  140. package/dist/moderation.js.map +1 -1
  141. package/dist/ollama.cjs +75 -5
  142. package/dist/ollama.cjs.map +1 -1
  143. package/dist/ollama.d.cts.map +1 -1
  144. package/dist/ollama.d.ts.map +1 -1
  145. package/dist/ollama.js +76 -6
  146. package/dist/ollama.js.map +1 -1
  147. package/dist/openrouter-video.cjs +4 -4
  148. package/dist/openrouter-video.cjs.map +1 -1
  149. package/dist/openrouter-video.d.cts.map +1 -1
  150. package/dist/openrouter-video.d.ts.map +1 -1
  151. package/dist/openrouter-video.js +4 -4
  152. package/dist/openrouter-video.js.map +1 -1
  153. package/dist/rerank.cjs +65 -2
  154. package/dist/rerank.cjs.map +1 -1
  155. package/dist/rerank.d.cts +1 -1
  156. package/dist/rerank.d.cts.map +1 -1
  157. package/dist/rerank.d.ts +1 -1
  158. package/dist/rerank.d.ts.map +1 -1
  159. package/dist/rerank.js +66 -3
  160. package/dist/rerank.js.map +1 -1
  161. package/dist/responses.cjs +18 -1
  162. package/dist/responses.cjs.map +1 -1
  163. package/dist/responses.d.cts.map +1 -1
  164. package/dist/responses.d.ts.map +1 -1
  165. package/dist/responses.js +19 -2
  166. package/dist/responses.js.map +1 -1
  167. package/dist/search.cjs +67 -3
  168. package/dist/search.cjs.map +1 -1
  169. package/dist/search.d.cts +1 -0
  170. package/dist/search.d.cts.map +1 -1
  171. package/dist/search.d.ts +1 -0
  172. package/dist/search.d.ts.map +1 -1
  173. package/dist/search.js +68 -4
  174. package/dist/search.js.map +1 -1
  175. package/dist/server.cjs +317 -13
  176. package/dist/server.cjs.map +1 -1
  177. package/dist/server.d.cts.map +1 -1
  178. package/dist/server.d.ts.map +1 -1
  179. package/dist/server.js +320 -16
  180. package/dist/server.js.map +1 -1
  181. package/dist/speech.cjs +18 -1
  182. package/dist/speech.cjs.map +1 -1
  183. package/dist/speech.d.cts.map +1 -1
  184. package/dist/speech.d.ts.map +1 -1
  185. package/dist/speech.js +19 -2
  186. package/dist/speech.js.map +1 -1
  187. package/dist/transcription.cjs +21 -2
  188. package/dist/transcription.cjs.map +1 -1
  189. package/dist/transcription.d.cts.map +1 -1
  190. package/dist/transcription.d.ts.map +1 -1
  191. package/dist/transcription.js +21 -2
  192. package/dist/transcription.js.map +1 -1
  193. package/dist/types.d.cts +39 -2
  194. package/dist/types.d.cts.map +1 -1
  195. package/dist/types.d.ts +39 -2
  196. package/dist/types.d.ts.map +1 -1
  197. package/dist/vector-mock.cjs +88 -0
  198. package/dist/vector-mock.cjs.map +1 -1
  199. package/dist/vector-mock.d.cts.map +1 -1
  200. package/dist/vector-mock.d.ts.map +1 -1
  201. package/dist/vector-mock.js +89 -1
  202. package/dist/vector-mock.js.map +1 -1
  203. package/dist/veo-video.cjs +2 -2
  204. package/dist/veo-video.cjs.map +1 -1
  205. package/dist/veo-video.d.cts.map +1 -1
  206. package/dist/veo-video.d.ts.map +1 -1
  207. package/dist/veo-video.js +2 -2
  208. package/dist/veo-video.js.map +1 -1
  209. package/dist/video.cjs +19 -2
  210. package/dist/video.cjs.map +1 -1
  211. package/dist/video.d.cts.map +1 -1
  212. package/dist/video.d.ts.map +1 -1
  213. package/dist/video.js +20 -3
  214. package/dist/video.js.map +1 -1
  215. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"search.js","names":[],"sources":["../src/search.ts"],"sourcesContent":["/**\n * Web Search API support for LLMock.\n *\n * Handles POST /search requests (Tavily-compatible). Matches fixtures by\n * comparing the request `query` field against registered patterns. First\n * match wins; no match returns empty results.\n */\n\nimport type * as http from \"node:http\";\nimport { flattenHeaders, matchesPattern } from \"./helpers.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\n\n// ─── Search types ─────────────────────────────────────────────────────────\n\nexport interface SearchResult {\n title: string;\n url: string;\n content: string;\n score?: number;\n}\n\nexport interface SearchFixture {\n match: string | RegExp;\n results: SearchResult[];\n}\n\n// ─── Request handler ──────────────────────────────────────────────────────\n\nexport async function handleSearch(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: SearchFixture[],\n journal: Journal,\n defaults: { logger: Logger },\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let body: { query?: string; max_results?: number };\n try {\n body = JSON.parse(raw) as { query?: string; max_results?: number };\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/search\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"search\",\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: `Malformed JSON: ${detail}`,\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return;\n }\n\n const query = body.query ?? \"\";\n const maxResults = body.max_results;\n\n // Find first matching fixture\n let matchedResults: SearchResult[] = [];\n let matchedFixture: SearchFixture | null = null;\n\n for (const fixture of fixtures) {\n if (matchesPattern(query, fixture.match)) {\n matchedFixture = fixture;\n matchedResults = fixture.results;\n break;\n }\n }\n\n if (matchedFixture) {\n logger.debug(`Search fixture matched for query \"${query.slice(0, 80)}\"`);\n } else {\n logger.debug(`No search fixture matched for query \"${query.slice(0, 80)}\" — returning empty`);\n }\n\n // Apply max_results limit\n if (maxResults !== undefined && maxResults > 0) {\n matchedResults = matchedResults.slice(0, maxResults);\n }\n\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/search\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"search\",\n response: { status: 200, fixture: null },\n });\n\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n query,\n results: matchedResults,\n images: [],\n response_time: 0,\n answer: null,\n }),\n );\n}\n"],"mappings":";;;AA6BA,eAAsB,aACpB,KACA,KACA,KACA,UACA,SACA,UACA,gBACe;CACf,MAAM,EAAE,WAAW;AACnB,gBAAe,IAAI;CAEnB,IAAI;AACJ,KAAI;AACF,SAAO,KAAK,MAAM,IAAI;UACf,UAAU;EACjB,MAAM,SAAS,oBAAoB,QAAQ,SAAS,UAAU;AAC9D,UAAQ,IAAI;GACV,QAAQ,IAAI,UAAU;GACtB,MAAM,IAAI,OAAO;GACjB,SAAS,eAAe,IAAI,QAAQ;GACpC,MAAM;GACN,SAAS;GACT,UAAU;IAAE,QAAQ;IAAK,SAAS;IAAM;GACzC,CAAC;AACF,MAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,MAAI,IACF,KAAK,UAAU,EACb,OAAO;GACL,SAAS,mBAAmB;GAC5B,MAAM;GACN,MAAM;GACP,EACF,CAAC,CACH;AACD;;CAGF,MAAM,QAAQ,KAAK,SAAS;CAC5B,MAAM,aAAa,KAAK;CAGxB,IAAI,iBAAiC,EAAE;CACvC,IAAI,iBAAuC;AAE3C,MAAK,MAAM,WAAW,SACpB,KAAI,eAAe,OAAO,QAAQ,MAAM,EAAE;AACxC,mBAAiB;AACjB,mBAAiB,QAAQ;AACzB;;AAIJ,KAAI,eACF,QAAO,MAAM,qCAAqC,MAAM,MAAM,GAAG,GAAG,CAAC,GAAG;KAExE,QAAO,MAAM,wCAAwC,MAAM,MAAM,GAAG,GAAG,CAAC,qBAAqB;AAI/F,KAAI,eAAe,UAAa,aAAa,EAC3C,kBAAiB,eAAe,MAAM,GAAG,WAAW;AAGtD,SAAQ,IAAI;EACV,QAAQ,IAAI,UAAU;EACtB,MAAM,IAAI,OAAO;EACjB,SAAS,eAAe,IAAI,QAAQ;EACpC,MAAM;EACN,SAAS;EACT,UAAU;GAAE,QAAQ;GAAK,SAAS;GAAM;EACzC,CAAC;AAEF,KAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,KAAI,IACF,KAAK,UAAU;EACb;EACA,SAAS;EACT,QAAQ,EAAE;EACV,eAAe;EACf,QAAQ;EACT,CAAC,CACH"}
1
+ {"version":3,"file":"search.js","names":[],"sources":["../src/search.ts"],"sourcesContent":["/**\n * Web Search API support for LLMock.\n *\n * Handles POST /search requests (Tavily-compatible). Matches fixtures by\n * comparing the request `query` field against registered patterns. First\n * match wins; no match returns empty results.\n */\n\nimport type * as http from \"node:http\";\nimport {\n flattenHeaders,\n isJsonObject,\n matchesPattern,\n normalizeTextInput,\n resolveStrictMode,\n strictNoMatchLogLine,\n strictNoMatchMessage,\n strictOverrideField,\n} from \"./helpers.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\n\n// ─── Search types ─────────────────────────────────────────────────────────\n\nexport interface SearchResult {\n title: string;\n url: string;\n content: string;\n score?: number;\n}\n\nexport interface SearchFixture {\n match: string | RegExp;\n results: SearchResult[];\n}\n\n// ─── Request handler ──────────────────────────────────────────────────────\n\nexport async function handleSearch(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: SearchFixture[],\n journal: Journal,\n defaults: { logger: Logger; strict?: boolean },\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let body: { query?: unknown; max_results?: number };\n try {\n body = JSON.parse(raw) as { query?: unknown; max_results?: number };\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/search\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"search\",\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: `Malformed JSON: ${detail}`,\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return;\n }\n\n // Reject bodies that parsed but are not a JSON object (e.g. `null`) before\n // touching fields — otherwise `body.query` throws a TypeError that surfaces\n // as a 500 instead of a 400.\n if (!isJsonObject(body)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/search\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"search\",\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: \"Request body must be a JSON object\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n const rawQuery: unknown = body.query ?? \"\";\n const normalizedQuery = normalizeTextInput(rawQuery);\n if (normalizedQuery === null) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/search\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"search\",\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: \"Invalid parameter: 'query' must be a string or an array of strings\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n // `query` is the normalized string used for matching; the response echoes\n // `rawQuery` so an array payload round-trips exactly as it did before.\n const query = normalizedQuery;\n const maxResults = body.max_results;\n\n // Find first matching fixture\n let matchedResults: SearchResult[] = [];\n let matchedFixture: SearchFixture | null = null;\n\n for (const fixture of fixtures) {\n if (matchesPattern(query, fixture.match)) {\n matchedFixture = fixture;\n matchedResults = fixture.results;\n break;\n }\n }\n\n if (matchedFixture) {\n logger.debug(`Search fixture matched for query \"${query.slice(0, 80)}\"`);\n } else {\n logger.debug(`No search fixture matched for query \"${query.slice(0, 80)}\" — returning empty`);\n }\n\n // Strict mode turns the lenient default (empty results) into a no-match\n // error, like every other fixture-matching handler. These fixtures carry no\n // sequence/turn state, so the skipped count is always 0.\n if (!matchedFixture && resolveStrictMode(defaults.strict, req.headers)) {\n const strictMessage = strictNoMatchMessage(0);\n logger.error(strictNoMatchLogLine(req.method ?? \"POST\", req.url ?? \"/search\", 0));\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/search\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"search\",\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n res.writeHead(503, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: strictMessage,\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n\n // Apply max_results limit\n if (maxResults !== undefined && maxResults > 0) {\n matchedResults = matchedResults.slice(0, maxResults);\n }\n\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/search\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"search\",\n response: {\n status: 200,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n query: rawQuery,\n results: matchedResults,\n images: [],\n response_time: 0,\n answer: null,\n }),\n );\n}\n"],"mappings":"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"}
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');
@@ -127,7 +128,8 @@ const DEFAULT_MODELS = [
127
128
  const CORS_HEADERS = {
128
129
  "Access-Control-Allow-Origin": "*",
129
130
  "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
130
- "Access-Control-Allow-Headers": "*"
131
+ "Access-Control-Allow-Headers": "*",
132
+ "Access-Control-Expose-Headers": "X-Total-Count"
131
133
  };
132
134
  function setCorsHeaders(res) {
133
135
  for (const [key, value] of Object.entries(CORS_HEADERS)) res.setHeader(key, value);
@@ -145,11 +147,24 @@ function handleNotFound(res, message) {
145
147
  } }));
146
148
  }
147
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
+ ]);
148
162
  /**
149
163
  * Perform a full reset: clear the fixtures array, the journal (entries *and*
150
164
  * per-test fixture match-counts, i.e. sequence position), the video and fal.ai
151
- * job/queue state, and the Gemini interaction/event-id counters, then re-zero
152
- * 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.
153
168
  *
154
169
  * `targets` is `null` when no server is running (an in-process `reset()` before
155
170
  * `start()`). The process-global generation state is reset either way, since it
@@ -166,6 +181,7 @@ function performFullReset(fixtures, targets) {
166
181
  require_gemini_interactions.resetEventIdCounter();
167
182
  if (!targets) return;
168
183
  targets.journal.clear();
184
+ targets.defaults.chaos = void 0;
169
185
  targets.videoStates.clear();
170
186
  targets.openRouterVideoJobs.clear();
171
187
  targets.veoVideoJobs.clear();
@@ -174,10 +190,103 @@ function performFullReset(fixtures, targets) {
174
190
  if (targets.defaults.registry) targets.defaults.registry.setGauge("aimock_fixtures_loaded", {}, fixtures.length);
175
191
  }
176
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
+ /**
177
286
  * Handle requests under `/__aimock/`. Returns `true` if the request was
178
287
  * handled, `false` if the path doesn't match the control prefix.
179
288
  */
180
- async function handleControlAPI(req, res, pathname, fixtures, journal, videoStates, openRouterVideoJobs, veoVideoJobs, grokVideoJobs, bytePlusVideoJobs, defaults) {
289
+ async function handleControlAPI(req, res, pathname, searchParams, fixtures, journal, videoStates, openRouterVideoJobs, veoVideoJobs, grokVideoJobs, bytePlusVideoJobs, defaults) {
181
290
  if (!pathname.startsWith(CONTROL_PREFIX)) return false;
182
291
  const subPath = pathname.slice(9);
183
292
  setCorsHeaders(res);
@@ -187,13 +296,148 @@ async function handleControlAPI(req, res, pathname, fixtures, journal, videoStat
187
296
  return true;
188
297
  }
189
298
  if (subPath === "/journal" && req.method === "GET") {
190
- res.writeHead(200, { "Content-Type": "application/json" });
191
- 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));
192
342
  return true;
193
343
  }
194
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);
377
+ res.writeHead(200, { "Content-Type": "application/json" });
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);
195
439
  res.writeHead(200, { "Content-Type": "application/json" });
196
- res.end(JSON.stringify({ count: fixtures.length }));
440
+ res.end(JSON.stringify({ chaos: effectiveChaos(defaults, scopeId) }));
197
441
  return true;
198
442
  }
199
443
  if (subPath === "/fixtures" && req.method === "POST") {
@@ -302,8 +546,37 @@ async function handleControlAPI(req, res, pathname, fixtures, journal, videoStat
302
546
  res.end(JSON.stringify({ error: `Invalid JSON: ${msg}` }));
303
547
  return true;
304
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
+ }
305
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
+ }
306
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
+ }
307
580
  const errorFixture = {
308
581
  match: { predicate: () => true },
309
582
  response: {
@@ -363,7 +636,6 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
363
636
  let body;
364
637
  try {
365
638
  body = JSON.parse(raw);
366
- if (modelFallback && !body.model) body.model = modelFallback;
367
639
  } catch (parseErr) {
368
640
  const detail = parseErr instanceof Error ? parseErr.message : "unknown parse error";
369
641
  journal.add({
@@ -384,6 +656,24 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
384
656
  } }));
385
657
  return;
386
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;
387
677
  if (!Array.isArray(body.messages)) {
388
678
  journal.add({
389
679
  method: req.method ?? "POST",
@@ -475,7 +765,7 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
475
765
  const snippet = typeof lastUserMsg?.content === "string" ? lastUserMsg.content.slice(0, 80) : "";
476
766
  defaults.logger.debug(`No fixture matched for request (model=${body.model ?? "?"}, msg="${snippet}")`);
477
767
  }
478
- 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);
479
769
  const chaosContext = {
480
770
  method,
481
771
  path,
@@ -772,13 +1062,27 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
772
1062
  const logger = new require_logger.Logger(options?.logLevel ?? "silent");
773
1063
  const registry = options?.metrics ? require_metrics.createMetricsRegistry() : void 0;
774
1064
  const serverOptions = options ?? {};
1065
+ const chaosByTestId = /* @__PURE__ */ new Map();
775
1066
  const defaults = {
776
1067
  latency: serverOptions.latency ?? 0,
777
1068
  chunkSize: Math.max(1, serverOptions.chunkSize ?? DEFAULT_CHUNK_SIZE),
778
1069
  replaySpeed: serverOptions.replaySpeed ?? 1,
779
1070
  logger,
1071
+ chaosByTestId,
780
1072
  get chaos() {
781
- 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);
782
1086
  },
783
1087
  registry,
784
1088
  get record() {
@@ -935,7 +1239,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
935
1239
  require_api_key_auth.markAuthenticatedRequest(req, resolvedAuth.policy);
936
1240
  }
937
1241
  if (pathname.startsWith(CONTROL_PREFIX)) {
938
- await handleControlAPI(req, res, pathname, fixtures, journal, videoStates, openRouterVideoJobs, veoVideoJobs, grokVideoJobs, bytePlusVideoJobs, defaults);
1242
+ await handleControlAPI(req, res, pathname, parsedUrl.searchParams, fixtures, journal, videoStates, openRouterVideoJobs, veoVideoJobs, grokVideoJobs, bytePlusVideoJobs, defaults);
939
1243
  return;
940
1244
  }
941
1245
  if (mounts) {
@@ -1827,7 +2131,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1827
2131
  try {
1828
2132
  falBody = req.method === "POST" || req.method === "PUT" ? await require_helpers.readBody(req) : "";
1829
2133
  const raw = falBody;
1830
- 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);
1831
2135
  if (chaosAction) {
1832
2136
  require_chaos.applyChaosAction(chaosAction, res, null, journal, {
1833
2137
  method: req.method ?? "GET",
@@ -1869,7 +2173,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
1869
2173
  setCorsHeaders(res);
1870
2174
  try {
1871
2175
  const raw = req.method === "POST" || req.method === "PUT" ? falBody ?? await require_helpers.readBody(req) : "{}";
1872
- 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);
1873
2177
  if (chaosAction) {
1874
2178
  require_chaos.applyChaosAction(chaosAction, res, null, journal, {
1875
2179
  method: req.method ?? "GET",