@copilotkit/aimock 1.43.0 → 1.45.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 (396) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +46 -0
  4. package/README.md +14 -2
  5. package/dist/agui-handler.d.ts.map +1 -1
  6. package/dist/aimock-cli.cjs +31 -5
  7. package/dist/aimock-cli.cjs.map +1 -1
  8. package/dist/aimock-cli.d.cts.map +1 -1
  9. package/dist/aimock-cli.d.ts.map +1 -1
  10. package/dist/aimock-cli.js +31 -5
  11. package/dist/aimock-cli.js.map +1 -1
  12. package/dist/bedrock-converse.cjs +344 -43
  13. package/dist/bedrock-converse.cjs.map +1 -1
  14. package/dist/bedrock-converse.d.cts.map +1 -1
  15. package/dist/bedrock-converse.d.ts.map +1 -1
  16. package/dist/bedrock-converse.js +345 -44
  17. package/dist/bedrock-converse.js.map +1 -1
  18. package/dist/bedrock.cjs +195 -42
  19. package/dist/bedrock.cjs.map +1 -1
  20. package/dist/bedrock.d.cts.map +1 -1
  21. package/dist/bedrock.d.ts.map +1 -1
  22. package/dist/bedrock.js +196 -43
  23. package/dist/bedrock.js.map +1 -1
  24. package/dist/chaos.cjs +9 -5
  25. package/dist/chaos.cjs.map +1 -1
  26. package/dist/chaos.d.cts.map +1 -1
  27. package/dist/chaos.d.ts.map +1 -1
  28. package/dist/chaos.js +10 -7
  29. package/dist/chaos.js.map +1 -1
  30. package/dist/cli.cjs +238 -9
  31. package/dist/cli.cjs.map +1 -1
  32. package/dist/cli.js +242 -13
  33. package/dist/cli.js.map +1 -1
  34. package/dist/cohere.cjs +159 -72
  35. package/dist/cohere.cjs.map +1 -1
  36. package/dist/cohere.d.cts.map +1 -1
  37. package/dist/cohere.d.ts.map +1 -1
  38. package/dist/cohere.js +160 -73
  39. package/dist/cohere.js.map +1 -1
  40. package/dist/config-loader.cjs +63 -5
  41. package/dist/config-loader.cjs.map +1 -1
  42. package/dist/config-loader.d.cts +37 -4
  43. package/dist/config-loader.d.cts.map +1 -1
  44. package/dist/config-loader.d.ts +37 -4
  45. package/dist/config-loader.d.ts.map +1 -1
  46. package/dist/config-loader.js +63 -5
  47. package/dist/config-loader.js.map +1 -1
  48. package/dist/constants.cjs +8 -0
  49. package/dist/constants.cjs.map +1 -1
  50. package/dist/constants.d.cts +6 -2
  51. package/dist/constants.d.ts +6 -2
  52. package/dist/constants.js +8 -1
  53. package/dist/constants.js.map +1 -1
  54. package/dist/echo-text.cjs +65 -0
  55. package/dist/echo-text.cjs.map +1 -0
  56. package/dist/echo-text.d.cts +22 -0
  57. package/dist/echo-text.d.cts.map +1 -0
  58. package/dist/echo-text.d.ts +22 -0
  59. package/dist/echo-text.d.ts.map +1 -0
  60. package/dist/echo-text.js +65 -0
  61. package/dist/echo-text.js.map +1 -0
  62. package/dist/embeddings.cjs +18 -1
  63. package/dist/embeddings.cjs.map +1 -1
  64. package/dist/embeddings.d.cts +1 -1
  65. package/dist/embeddings.d.cts.map +1 -1
  66. package/dist/embeddings.d.ts +1 -1
  67. package/dist/embeddings.d.ts.map +1 -1
  68. package/dist/embeddings.js +18 -1
  69. package/dist/embeddings.js.map +1 -1
  70. package/dist/fakes-plugin.cjs +131 -0
  71. package/dist/fakes-plugin.cjs.map +1 -0
  72. package/dist/fakes-plugin.d.cts +23 -0
  73. package/dist/fakes-plugin.d.cts.map +1 -0
  74. package/dist/fakes-plugin.d.ts +23 -0
  75. package/dist/fakes-plugin.d.ts.map +1 -0
  76. package/dist/fakes-plugin.js +127 -0
  77. package/dist/fakes-plugin.js.map +1 -0
  78. package/dist/files.cjs +9 -11
  79. package/dist/files.cjs.map +1 -1
  80. package/dist/files.d.cts.map +1 -1
  81. package/dist/files.d.ts.map +1 -1
  82. package/dist/files.js +10 -13
  83. package/dist/files.js.map +1 -1
  84. package/dist/fine-tuning.cjs +1 -19
  85. package/dist/fine-tuning.cjs.map +1 -1
  86. package/dist/fine-tuning.d.cts.map +1 -1
  87. package/dist/fine-tuning.d.ts.map +1 -1
  88. package/dist/fine-tuning.js +2 -20
  89. package/dist/fine-tuning.js.map +1 -1
  90. package/dist/fixture-loader-services.cjs +116 -0
  91. package/dist/fixture-loader-services.cjs.map +1 -0
  92. package/dist/fixture-loader-services.d.cts +48 -0
  93. package/dist/fixture-loader-services.d.cts.map +1 -0
  94. package/dist/fixture-loader-services.d.ts +48 -0
  95. package/dist/fixture-loader-services.d.ts.map +1 -0
  96. package/dist/fixture-loader-services.js +114 -0
  97. package/dist/fixture-loader-services.js.map +1 -0
  98. package/dist/fixture-loader.cjs +552 -29
  99. package/dist/fixture-loader.cjs.map +1 -1
  100. package/dist/fixture-loader.d.cts +140 -2
  101. package/dist/fixture-loader.d.cts.map +1 -1
  102. package/dist/fixture-loader.d.ts +140 -2
  103. package/dist/fixture-loader.d.ts.map +1 -1
  104. package/dist/fixture-loader.js +542 -31
  105. package/dist/fixture-loader.js.map +1 -1
  106. package/dist/gemini-embeddings.cjs +42 -2
  107. package/dist/gemini-embeddings.cjs.map +1 -1
  108. package/dist/gemini-embeddings.js +43 -3
  109. package/dist/gemini-embeddings.js.map +1 -1
  110. package/dist/gemini-interactions.cjs +288 -52
  111. package/dist/gemini-interactions.cjs.map +1 -1
  112. package/dist/gemini-interactions.d.cts.map +1 -1
  113. package/dist/gemini-interactions.d.ts.map +1 -1
  114. package/dist/gemini-interactions.js +289 -53
  115. package/dist/gemini-interactions.js.map +1 -1
  116. package/dist/gemini.cjs +234 -38
  117. package/dist/gemini.cjs.map +1 -1
  118. package/dist/gemini.d.cts.map +1 -1
  119. package/dist/gemini.d.ts.map +1 -1
  120. package/dist/gemini.js +235 -39
  121. package/dist/gemini.js.map +1 -1
  122. package/dist/helpers.cjs +1157 -12
  123. package/dist/helpers.cjs.map +1 -1
  124. package/dist/helpers.d.cts +85 -4
  125. package/dist/helpers.d.cts.map +1 -1
  126. package/dist/helpers.d.ts +85 -4
  127. package/dist/helpers.d.ts.map +1 -1
  128. package/dist/helpers.js +1113 -14
  129. package/dist/helpers.js.map +1 -1
  130. package/dist/images.cjs +3 -2
  131. package/dist/images.cjs.map +1 -1
  132. package/dist/images.js +3 -2
  133. package/dist/images.js.map +1 -1
  134. package/dist/index.cjs +39 -1
  135. package/dist/index.d.cts +11 -4
  136. package/dist/index.d.ts +11 -4
  137. package/dist/index.js +11 -4
  138. package/dist/jest.cjs +43 -14
  139. package/dist/jest.cjs.map +1 -1
  140. package/dist/jest.d.cts +47 -5
  141. package/dist/jest.d.cts.map +1 -1
  142. package/dist/jest.d.ts +47 -5
  143. package/dist/jest.d.ts.map +1 -1
  144. package/dist/jest.js +45 -16
  145. package/dist/jest.js.map +1 -1
  146. package/dist/journal.cjs +155 -13
  147. package/dist/journal.cjs.map +1 -1
  148. package/dist/journal.d.cts +54 -4
  149. package/dist/journal.d.cts.map +1 -1
  150. package/dist/journal.d.ts +54 -4
  151. package/dist/journal.d.ts.map +1 -1
  152. package/dist/journal.js +155 -13
  153. package/dist/journal.js.map +1 -1
  154. package/dist/live-sanitize.cjs +4 -32
  155. package/dist/live-sanitize.cjs.map +1 -1
  156. package/dist/live-sanitize.js +4 -32
  157. package/dist/live-sanitize.js.map +1 -1
  158. package/dist/llmock.cjs +204 -39
  159. package/dist/llmock.cjs.map +1 -1
  160. package/dist/llmock.d.cts +51 -1
  161. package/dist/llmock.d.cts.map +1 -1
  162. package/dist/llmock.d.ts +51 -1
  163. package/dist/llmock.d.ts.map +1 -1
  164. package/dist/llmock.js +206 -41
  165. package/dist/llmock.js.map +1 -1
  166. package/dist/mcp-fakes-mount.cjs +217 -0
  167. package/dist/mcp-fakes-mount.cjs.map +1 -0
  168. package/dist/mcp-fakes-mount.js +211 -0
  169. package/dist/mcp-fakes-mount.js.map +1 -0
  170. package/dist/mcp-fakes-report.cjs +111 -0
  171. package/dist/mcp-fakes-report.cjs.map +1 -0
  172. package/dist/mcp-fakes-report.d.cts +40 -0
  173. package/dist/mcp-fakes-report.d.cts.map +1 -0
  174. package/dist/mcp-fakes-report.d.ts +40 -0
  175. package/dist/mcp-fakes-report.d.ts.map +1 -0
  176. package/dist/mcp-fakes-report.js +107 -0
  177. package/dist/mcp-fakes-report.js.map +1 -0
  178. package/dist/mcp-fakes.cjs +1942 -0
  179. package/dist/mcp-fakes.cjs.map +1 -0
  180. package/dist/mcp-fakes.d.cts +504 -0
  181. package/dist/mcp-fakes.d.cts.map +1 -0
  182. package/dist/mcp-fakes.d.ts +504 -0
  183. package/dist/mcp-fakes.d.ts.map +1 -0
  184. package/dist/mcp-fakes.js +1937 -0
  185. package/dist/mcp-fakes.js.map +1 -0
  186. package/dist/mcp-handler.cjs +534 -27
  187. package/dist/mcp-handler.cjs.map +1 -1
  188. package/dist/mcp-handler.d.cts +49 -0
  189. package/dist/mcp-handler.d.cts.map +1 -0
  190. package/dist/mcp-handler.d.ts +49 -0
  191. package/dist/mcp-handler.d.ts.map +1 -0
  192. package/dist/mcp-handler.js +530 -28
  193. package/dist/mcp-handler.js.map +1 -1
  194. package/dist/mcp-mock.cjs +326 -47
  195. package/dist/mcp-mock.cjs.map +1 -1
  196. package/dist/mcp-mock.d.cts +97 -1
  197. package/dist/mcp-mock.d.cts.map +1 -1
  198. package/dist/mcp-mock.d.ts +97 -1
  199. package/dist/mcp-mock.d.ts.map +1 -1
  200. package/dist/mcp-mock.js +327 -48
  201. package/dist/mcp-mock.js.map +1 -1
  202. package/dist/mcp-recorder.cjs +1224 -0
  203. package/dist/mcp-recorder.cjs.map +1 -0
  204. package/dist/mcp-recorder.d.cts +108 -0
  205. package/dist/mcp-recorder.d.cts.map +1 -0
  206. package/dist/mcp-recorder.d.ts +108 -0
  207. package/dist/mcp-recorder.d.ts.map +1 -0
  208. package/dist/mcp-recorder.js +1215 -0
  209. package/dist/mcp-recorder.js.map +1 -0
  210. package/dist/mcp-stub.cjs +5 -1
  211. package/dist/mcp-stub.d.cts +3 -1
  212. package/dist/mcp-stub.d.ts +3 -1
  213. package/dist/mcp-stub.js +3 -1
  214. package/dist/mcp-types.d.cts +6 -0
  215. package/dist/mcp-types.d.ts +6 -0
  216. package/dist/message-text.cjs +192 -0
  217. package/dist/message-text.cjs.map +1 -0
  218. package/dist/message-text.d.cts +31 -0
  219. package/dist/message-text.d.cts.map +1 -0
  220. package/dist/message-text.d.ts +31 -0
  221. package/dist/message-text.d.ts.map +1 -0
  222. package/dist/message-text.js +184 -0
  223. package/dist/message-text.js.map +1 -0
  224. package/dist/messages.cjs +400 -56
  225. package/dist/messages.cjs.map +1 -1
  226. package/dist/messages.d.cts.map +1 -1
  227. package/dist/messages.d.ts.map +1 -1
  228. package/dist/messages.js +401 -58
  229. package/dist/messages.js.map +1 -1
  230. package/dist/metrics.cjs +46 -66
  231. package/dist/metrics.cjs.map +1 -1
  232. package/dist/metrics.d.cts +14 -6
  233. package/dist/metrics.d.cts.map +1 -1
  234. package/dist/metrics.d.ts +14 -6
  235. package/dist/metrics.d.ts.map +1 -1
  236. package/dist/metrics.js +32 -40
  237. package/dist/metrics.js.map +1 -1
  238. package/dist/misbehavior.cjs +1523 -0
  239. package/dist/misbehavior.cjs.map +1 -0
  240. package/dist/misbehavior.d.cts +43 -0
  241. package/dist/misbehavior.d.cts.map +1 -0
  242. package/dist/misbehavior.d.ts +43 -0
  243. package/dist/misbehavior.d.ts.map +1 -0
  244. package/dist/misbehavior.js +1511 -0
  245. package/dist/misbehavior.js.map +1 -0
  246. package/dist/ollama.cjs +315 -113
  247. package/dist/ollama.cjs.map +1 -1
  248. package/dist/ollama.d.cts.map +1 -1
  249. package/dist/ollama.d.ts.map +1 -1
  250. package/dist/ollama.js +316 -114
  251. package/dist/ollama.js.map +1 -1
  252. package/dist/openapi.cjs +3405 -0
  253. package/dist/openapi.cjs.map +1 -0
  254. package/dist/openapi.d.cts +30 -0
  255. package/dist/openapi.d.cts.map +1 -0
  256. package/dist/openapi.d.ts +30 -0
  257. package/dist/openapi.d.ts.map +1 -0
  258. package/dist/openapi.js +3404 -0
  259. package/dist/openapi.js.map +1 -0
  260. package/dist/openrouter-video.cjs +4 -4
  261. package/dist/openrouter-video.cjs.map +1 -1
  262. package/dist/openrouter-video.d.cts.map +1 -1
  263. package/dist/openrouter-video.d.ts.map +1 -1
  264. package/dist/openrouter-video.js +4 -4
  265. package/dist/openrouter-video.js.map +1 -1
  266. package/dist/record-sanitize.cjs +231 -0
  267. package/dist/record-sanitize.cjs.map +1 -0
  268. package/dist/record-sanitize.js +224 -0
  269. package/dist/record-sanitize.js.map +1 -0
  270. package/dist/recorder.cjs +115 -6
  271. package/dist/recorder.cjs.map +1 -1
  272. package/dist/recorder.d.cts +2 -1
  273. package/dist/recorder.d.cts.map +1 -1
  274. package/dist/recorder.d.ts +2 -1
  275. package/dist/recorder.d.ts.map +1 -1
  276. package/dist/recorder.js +116 -8
  277. package/dist/recorder.js.map +1 -1
  278. package/dist/responses.cjs +701 -166
  279. package/dist/responses.cjs.map +1 -1
  280. package/dist/responses.d.cts +5 -13
  281. package/dist/responses.d.cts.map +1 -1
  282. package/dist/responses.d.ts +5 -13
  283. package/dist/responses.d.ts.map +1 -1
  284. package/dist/responses.js +699 -169
  285. package/dist/responses.js.map +1 -1
  286. package/dist/route-registry.cjs +951 -0
  287. package/dist/route-registry.cjs.map +1 -0
  288. package/dist/route-registry.d.cts +33 -0
  289. package/dist/route-registry.d.cts.map +1 -0
  290. package/dist/route-registry.d.ts +33 -0
  291. package/dist/route-registry.d.ts.map +1 -0
  292. package/dist/route-registry.js +866 -0
  293. package/dist/route-registry.js.map +1 -0
  294. package/dist/router.cjs +24 -2
  295. package/dist/router.cjs.map +1 -1
  296. package/dist/router.d.cts.map +1 -1
  297. package/dist/router.d.ts.map +1 -1
  298. package/dist/router.js +25 -4
  299. package/dist/router.js.map +1 -1
  300. package/dist/server.cjs +1159 -264
  301. package/dist/server.cjs.map +1 -1
  302. package/dist/server.d.cts +4 -1
  303. package/dist/server.d.cts.map +1 -1
  304. package/dist/server.d.ts +4 -1
  305. package/dist/server.d.ts.map +1 -1
  306. package/dist/server.js +1067 -172
  307. package/dist/server.js.map +1 -1
  308. package/dist/speech.cjs +4 -2
  309. package/dist/speech.cjs.map +1 -1
  310. package/dist/speech.js +4 -2
  311. package/dist/speech.js.map +1 -1
  312. package/dist/stream-collapse.cjs +236 -34
  313. package/dist/stream-collapse.cjs.map +1 -1
  314. package/dist/stream-collapse.d.cts +39 -4
  315. package/dist/stream-collapse.d.cts.map +1 -1
  316. package/dist/stream-collapse.d.ts +39 -4
  317. package/dist/stream-collapse.d.ts.map +1 -1
  318. package/dist/stream-collapse.js +235 -35
  319. package/dist/stream-collapse.js.map +1 -1
  320. package/dist/suite.cjs +1 -1
  321. package/dist/suite.js +1 -1
  322. package/dist/transcription.cjs +2 -2
  323. package/dist/transcription.cjs.map +1 -1
  324. package/dist/transcription.d.cts.map +1 -1
  325. package/dist/transcription.d.ts.map +1 -1
  326. package/dist/transcription.js +2 -2
  327. package/dist/transcription.js.map +1 -1
  328. package/dist/types.cjs +18 -0
  329. package/dist/types.cjs.map +1 -0
  330. package/dist/types.d.cts +516 -5
  331. package/dist/types.d.cts.map +1 -1
  332. package/dist/types.d.ts +516 -5
  333. package/dist/types.d.ts.map +1 -1
  334. package/dist/types.js +17 -0
  335. package/dist/types.js.map +1 -0
  336. package/dist/validate-cli.cjs +123 -33
  337. package/dist/validate-cli.cjs.map +1 -1
  338. package/dist/validate-cli.js +125 -35
  339. package/dist/validate-cli.js.map +1 -1
  340. package/dist/vector-stores.cjs +1180 -0
  341. package/dist/vector-stores.cjs.map +1 -0
  342. package/dist/vector-stores.d.cts +106 -0
  343. package/dist/vector-stores.d.cts.map +1 -0
  344. package/dist/vector-stores.d.ts +106 -0
  345. package/dist/vector-stores.d.ts.map +1 -0
  346. package/dist/vector-stores.js +1164 -0
  347. package/dist/vector-stores.js.map +1 -0
  348. package/dist/vector-types.d.cts.map +1 -1
  349. package/dist/version.cjs +45 -0
  350. package/dist/version.cjs.map +1 -0
  351. package/dist/version.js +44 -0
  352. package/dist/version.js.map +1 -0
  353. package/dist/video.cjs +19 -0
  354. package/dist/video.cjs.map +1 -1
  355. package/dist/video.d.cts.map +1 -1
  356. package/dist/video.d.ts.map +1 -1
  357. package/dist/video.js +19 -0
  358. package/dist/video.js.map +1 -1
  359. package/dist/vitest.cjs +77 -16
  360. package/dist/vitest.cjs.map +1 -1
  361. package/dist/vitest.d.cts +38 -5
  362. package/dist/vitest.d.cts.map +1 -1
  363. package/dist/vitest.d.ts +38 -5
  364. package/dist/vitest.d.ts.map +1 -1
  365. package/dist/vitest.js +78 -17
  366. package/dist/vitest.js.map +1 -1
  367. package/dist/watcher.cjs +6 -2
  368. package/dist/watcher.cjs.map +1 -1
  369. package/dist/watcher.js +6 -2
  370. package/dist/watcher.js.map +1 -1
  371. package/dist/ws-gemini-live.cjs +210 -101
  372. package/dist/ws-gemini-live.cjs.map +1 -1
  373. package/dist/ws-gemini-live.d.cts +9 -3
  374. package/dist/ws-gemini-live.d.cts.map +1 -1
  375. package/dist/ws-gemini-live.d.ts +9 -3
  376. package/dist/ws-gemini-live.d.ts.map +1 -1
  377. package/dist/ws-gemini-live.js +211 -102
  378. package/dist/ws-gemini-live.js.map +1 -1
  379. package/dist/ws-realtime.cjs +175 -62
  380. package/dist/ws-realtime.cjs.map +1 -1
  381. package/dist/ws-realtime.d.cts +8 -3
  382. package/dist/ws-realtime.d.cts.map +1 -1
  383. package/dist/ws-realtime.d.ts +8 -3
  384. package/dist/ws-realtime.d.ts.map +1 -1
  385. package/dist/ws-realtime.js +176 -63
  386. package/dist/ws-realtime.js.map +1 -1
  387. package/dist/ws-responses.cjs +121 -16
  388. package/dist/ws-responses.cjs.map +1 -1
  389. package/dist/ws-responses.d.cts +8 -12
  390. package/dist/ws-responses.d.cts.map +1 -1
  391. package/dist/ws-responses.d.ts +8 -12
  392. package/dist/ws-responses.d.ts.map +1 -1
  393. package/dist/ws-responses.js +123 -18
  394. package/dist/ws-responses.js.map +1 -1
  395. package/package.json +13 -2
  396. package/skills/write-fixtures/SKILL.md +179 -21
@@ -0,0 +1,65 @@
1
+ import { MCP_FAKES_ECHO_LIMIT } from "./constants.js";
2
+
3
+ //#region src/echo-text.ts
4
+ /**
5
+ * The one cut rule for text that MCP fakes put into a message. The message
6
+ * builder (`build` in message-text.ts) cuts every user part of a message with
7
+ * it, once; helpers.ts also cuts the `MCP_INVALID_UNDECLARED` `raw` field
8
+ * with it. A leaf module (it imports only constants.ts), so every one of
9
+ * those can use it without an import cycle.
10
+ */
11
+ /**
12
+ * `text` cut to at most `limit` chars in all: the kept part, then `…` and the
13
+ * count of chars left out. When even that count does not fit in `limit`, the
14
+ * kept part is followed by a bare `…`. A `limit` below 1 has no room even for
15
+ * that `…`, so a text longer than it is cut to `""`; the message builder
16
+ * never asks for that (each of its user parts gets at least 1 char). The
17
+ * kept part is a prefix of `text` that never ends inside a unit of `units`. A
18
+ * `limit` that is not an integer is rounded down, a negative one
19
+ * (`-Infinity` too) is 0, and a `NaN` limit is a `RangeError`.
20
+ *
21
+ * @internal
22
+ */
23
+ function cutText(text, limit = MCP_FAKES_ECHO_LIMIT, units = "text") {
24
+ if (Number.isNaN(limit)) throw new RangeError("cutText limit must be a number, got NaN");
25
+ limit = Math.max(0, Math.floor(limit));
26
+ if (text.length <= limit) return text;
27
+ let keep = limit;
28
+ let suffix = "";
29
+ for (;;) {
30
+ suffix = `… (${text.length - keep} more chars)`;
31
+ const room = Math.max(0, limit - suffix.length);
32
+ if (keep <= room) break;
33
+ keep = room;
34
+ }
35
+ if (keep + suffix.length > limit) return limit < 1 ? "" : text.slice(0, safeEnd(text, limit - 1, units)) + "…";
36
+ keep = safeEnd(text, keep, units);
37
+ return text.slice(0, keep) + `… (${text.length - keep} more chars)`;
38
+ }
39
+ /** `keep`, moved back to a point that does not split a unit of `units`. */
40
+ function safeEnd(text, keep, units) {
41
+ return surrogateSafeEnd(text, units === "json" ? escapeSafeEnd(text, keep) : keep);
42
+ }
43
+ /** `keep`, or one less when the char before it is a high surrogate. */
44
+ function surrogateSafeEnd(text, keep) {
45
+ if (keep <= 0) return keep;
46
+ const code = text.charCodeAt(keep - 1);
47
+ return code >= 55296 && code <= 56319 ? keep - 1 : keep;
48
+ }
49
+ /**
50
+ * `keep`, or the start of the JSON escape (`\x` or `\uXXXX`) that a cut at
51
+ * `keep` would split.
52
+ */
53
+ function escapeSafeEnd(text, keep) {
54
+ for (let i = 0; i < keep; i++) {
55
+ if (text.charCodeAt(i) !== 92) continue;
56
+ const end = i + (text[i + 1] === "u" ? 6 : 2);
57
+ if (end > keep) return i;
58
+ i = end - 1;
59
+ }
60
+ return keep;
61
+ }
62
+
63
+ //#endregion
64
+ export { cutText };
65
+ //# sourceMappingURL=echo-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"echo-text.js","names":[],"sources":["../src/echo-text.ts"],"sourcesContent":["/**\n * The one cut rule for text that MCP fakes put into a message. The message\n * builder (`build` in message-text.ts) cuts every user part of a message with\n * it, once; helpers.ts also cuts the `MCP_INVALID_UNDECLARED` `raw` field\n * with it. A leaf module (it imports only constants.ts), so every one of\n * those can use it without an import cycle.\n */\nimport { MCP_FAKES_ECHO_LIMIT } from \"./constants.js\";\n\n/**\n * What a cut must not split. `\"text\"`: a surrogate pair. `\"json\"`: a\n * surrogate pair or a JSON escape (`\\x` or `\\uXXXX`); for JSON text, where a\n * backslash only ever starts an escape.\n */\nexport type CutUnits = \"text\" | \"json\";\n\n/**\n * `text` cut to at most `limit` chars in all: the kept part, then `…` and the\n * count of chars left out. When even that count does not fit in `limit`, the\n * kept part is followed by a bare `…`. A `limit` below 1 has no room even for\n * that `…`, so a text longer than it is cut to `\"\"`; the message builder\n * never asks for that (each of its user parts gets at least 1 char). The\n * kept part is a prefix of `text` that never ends inside a unit of `units`. A\n * `limit` that is not an integer is rounded down, a negative one\n * (`-Infinity` too) is 0, and a `NaN` limit is a `RangeError`.\n *\n * @internal\n */\nexport function cutText(\n text: string,\n limit: number = MCP_FAKES_ECHO_LIMIT,\n units: CutUnits = \"text\",\n): string {\n if (Number.isNaN(limit)) throw new RangeError(\"cutText limit must be a number, got NaN\");\n // A negative limit (`-Infinity` included) keeps nothing, as a limit of 0 does.\n limit = Math.max(0, Math.floor(limit));\n if (text.length <= limit) return text;\n let keep = limit;\n let suffix = \"\";\n for (;;) {\n suffix = `… (${text.length - keep} more chars)`;\n const room = Math.max(0, limit - suffix.length);\n if (keep <= room) break;\n keep = room;\n }\n if (keep + suffix.length > limit) {\n // Not even the count fits: keep what fits before a bare `…`.\n return limit < 1 ? \"\" : text.slice(0, safeEnd(text, limit - 1, units)) + \"…\";\n }\n // Moving the cut back by d chars grows the count by d, which adds at most\n // one digit to the suffix, so the total stays within `limit`.\n keep = safeEnd(text, keep, units);\n return text.slice(0, keep) + `… (${text.length - keep} more chars)`;\n}\n\n/** `keep`, moved back to a point that does not split a unit of `units`. */\nfunction safeEnd(text: string, keep: number, units: CutUnits): number {\n return surrogateSafeEnd(text, units === \"json\" ? escapeSafeEnd(text, keep) : keep);\n}\n\n/** `keep`, or one less when the char before it is a high surrogate. */\nfunction surrogateSafeEnd(text: string, keep: number): number {\n if (keep <= 0) return keep;\n const code = text.charCodeAt(keep - 1);\n return code >= 0xd800 && code <= 0xdbff ? keep - 1 : keep;\n}\n\n/**\n * `keep`, or the start of the JSON escape (`\\x` or `\\uXXXX`) that a cut at\n * `keep` would split.\n */\nfunction escapeSafeEnd(text: string, keep: number): number {\n for (let i = 0; i < keep; i++) {\n if (text.charCodeAt(i) !== 0x5c) continue;\n const end = i + (text[i + 1] === \"u\" ? 6 : 2);\n if (end > keep) return i;\n i = end - 1;\n }\n return keep;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAgB,QACd,MACA,QAAgB,sBAChB,QAAkB,QACV;AACR,KAAI,OAAO,MAAM,MAAM,CAAE,OAAM,IAAI,WAAW,0CAA0C;AAExF,SAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,CAAC;AACtC,KAAI,KAAK,UAAU,MAAO,QAAO;CACjC,IAAI,OAAO;CACX,IAAI,SAAS;AACb,UAAS;AACP,WAAS,MAAM,KAAK,SAAS,KAAK;EAClC,MAAM,OAAO,KAAK,IAAI,GAAG,QAAQ,OAAO,OAAO;AAC/C,MAAI,QAAQ,KAAM;AAClB,SAAO;;AAET,KAAI,OAAO,OAAO,SAAS,MAEzB,QAAO,QAAQ,IAAI,KAAK,KAAK,MAAM,GAAG,QAAQ,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG;AAI3E,QAAO,QAAQ,MAAM,MAAM,MAAM;AACjC,QAAO,KAAK,MAAM,GAAG,KAAK,GAAG,MAAM,KAAK,SAAS,KAAK;;;AAIxD,SAAS,QAAQ,MAAc,MAAc,OAAyB;AACpE,QAAO,iBAAiB,MAAM,UAAU,SAAS,cAAc,MAAM,KAAK,GAAG,KAAK;;;AAIpF,SAAS,iBAAiB,MAAc,MAAsB;AAC5D,KAAI,QAAQ,EAAG,QAAO;CACtB,MAAM,OAAO,KAAK,WAAW,OAAO,EAAE;AACtC,QAAO,QAAQ,SAAU,QAAQ,QAAS,OAAO,IAAI;;;;;;AAOvD,SAAS,cAAc,MAAc,MAAsB;AACzD,MAAK,IAAI,IAAI,GAAG,IAAI,MAAM,KAAK;AAC7B,MAAI,KAAK,WAAW,EAAE,KAAK,GAAM;EACjC,MAAM,MAAM,KAAK,KAAK,IAAI,OAAO,MAAM,IAAI;AAC3C,MAAI,MAAM,KAAM,QAAO;AACvB,MAAI,MAAM;;AAEZ,QAAO"}
@@ -6,7 +6,7 @@ const require_router = require('./router.cjs');
6
6
  const require_recorder = require('./recorder.cjs');
7
7
 
8
8
  //#region src/embeddings.ts
9
- async function handleEmbeddings(req, res, raw, fixtures, journal, defaults, setCorsHeaders, providerKey = "openai") {
9
+ async function handleEmbeddings(req, res, raw, fixtures, journal, defaults, setCorsHeaders, providerKey = "openai", requireModel = false) {
10
10
  const { logger } = defaults;
11
11
  setCorsHeaders(res);
12
12
  let embeddingReq;
@@ -65,6 +65,23 @@ async function handleEmbeddings(req, res, raw, fixtures, journal, defaults, setC
65
65
  } }));
66
66
  return;
67
67
  }
68
+ if (requireModel && embeddingReq.model === void 0) {
69
+ journal.add({
70
+ method: req.method ?? "POST",
71
+ path: req.url ?? "/v1/embeddings",
72
+ headers: require_helpers.flattenHeaders(req.headers),
73
+ body: null,
74
+ response: {
75
+ status: 400,
76
+ fixture: null
77
+ }
78
+ });
79
+ require_sse_writer.writeErrorResponse(res, 400, JSON.stringify({ error: {
80
+ message: "Missing required parameter: 'model'",
81
+ type: "invalid_request_error"
82
+ } }));
83
+ return;
84
+ }
68
85
  const inputs = require_helpers.normalizeEmbeddingInput(embeddingReq.input);
69
86
  if (inputs === null) {
70
87
  journal.add({
@@ -1 +1 @@
1
- {"version":3,"file":"embeddings.cjs","names":["flattenHeaders","isJsonObject","normalizeEmbeddingInput","getContext","getTestId","selectFixtureForServing","applyChaosAsync","resolveResponse","isErrorResponse","serializeErrorResponse","isEmbeddingResponse","buildEmbeddingResponse","resolveStrictMode","strictNoMatchMessage","strictNoMatchLogLine","strictOverrideField","proxyAndRecord","validateEmbeddingDimensions","MAX_EMBEDDING_DIMENSIONS","generateDeterministicEmbedding"],"sources":["../src/embeddings.ts"],"sourcesContent":["/**\n * OpenAI Embeddings API support for aimock.\n *\n * Handles POST /v1/embeddings requests. Matches fixtures using the `inputText`\n * field, and falls back to generating a deterministic embedding from the input\n * text hash when no fixture matches.\n */\n\nimport type * as http from \"node:http\";\nimport type {\n ChatCompletionRequest,\n Fixture,\n HandlerDefaults,\n RecordProviderKey,\n} from \"./types.js\";\nimport {\n isEmbeddingResponse,\n isErrorResponse,\n serializeErrorResponse,\n generateDeterministicEmbedding,\n buildEmbeddingResponse,\n flattenHeaders,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n getContext,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n validateEmbeddingDimensions,\n isJsonObject,\n normalizeEmbeddingInput,\n MAX_EMBEDDING_DIMENSIONS,\n} from \"./helpers.js\";\nimport { releaseOneShotError } from \"./fixture-loader.js\";\nimport { selectFixtureForServing } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaosAsync } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\n// ─── Embeddings API request types ──────────────────────────────────────────\n\ninterface EmbeddingRequest {\n // Validated at runtime — these arrive as arbitrary JSON, so the declared\n // type must not claim a shape the parser cannot guarantee.\n input: unknown;\n model: string;\n encoding_format?: \"float\" | \"base64\";\n dimensions?: unknown;\n [key: string]: unknown;\n}\n\n// ─── Request handler ───────────────────────────────────────────────────────\n\nexport async function handleEmbeddings(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n providerKey: RecordProviderKey = \"openai\",\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let embeddingReq: EmbeddingRequest;\n try {\n embeddingReq = JSON.parse(raw) as EmbeddingRequest;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: `Malformed JSON body: ${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 `embeddingReq.input` throws a TypeError that\n // surfaces as a 500 instead of a 400.\n if (!isJsonObject(embeddingReq)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\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 // Validate required input parameter\n if (embeddingReq.input === undefined || embeddingReq.input === null) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: \"Missing required parameter: 'input'\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n // Normalize input to array of strings — reject non-string inputs with 400\n // instead of crashing downstream in createHash().update() (500).\n const inputs = normalizeEmbeddingInput(embeddingReq.input);\n if (inputs === null) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message:\n \"Invalid parameter: 'input' must be a string, an array of strings, or an array of tokens\",\n type: \"invalid_request_error\",\n param: null,\n code: null,\n },\n }),\n );\n return;\n }\n\n // Concatenate all inputs for matching purposes\n const combinedInput = inputs.join(\" \");\n\n // Build a synthetic ChatCompletionRequest for the fixture router.\n // We attach `embeddingInput` so the router's inputText matching can use it.\n const syntheticReq: ChatCompletionRequest = {\n model: embeddingReq.model,\n messages: [],\n embeddingInput: combinedInput,\n _endpointType: \"embedding\",\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const { fixture, skippedBySequenceOrTurn } = selectFixtureForServing(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n logger.debug(`No fixture matched for request`);\n }\n\n if (\n await applyChaosAsync(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n {\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n ) {\n releaseOneShotError(fixtures, fixture);\n return;\n }\n\n if (fixture) {\n const response = await resolveResponse(fixture, syntheticReq);\n\n // Error response\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, serializeErrorResponse(response), {\n retryAfter: response.retryAfter,\n });\n return;\n }\n\n // Embedding response — use the fixture's embedding for each input\n if (isEmbeddingResponse(response)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n const embeddings = inputs.map(() => [...response.embedding]);\n const body = buildEmbeddingResponse(embeddings, embeddingReq.model);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n return;\n }\n\n // Fixture matched but response type is not compatible with embeddings\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message:\n \"Fixture response did not match any known embedding type (must have embedding or error)\",\n type: \"server_error\",\n },\n }),\n );\n return;\n }\n\n // No fixture match — check strict mode first, then try proxy\n if (resolveStrictMode(defaults.strict, req.headers)) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n logger.error(\n strictNoMatchLogLine(\n req.method ?? \"POST\",\n req.url ?? \"/v1/embeddings\",\n skippedBySequenceOrTurn,\n ),\n );\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\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 if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n providerKey,\n req.url ?? \"/v1/embeddings\",\n fixtures,\n defaults,\n raw,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome !== \"not_configured\") {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return;\n }\n }\n\n // No fixture match — generate deterministic embeddings from input text\n logger.warn(\n `No embedding fixture matched for \"${combinedInput.slice(0, 80)}\" — returning deterministic fallback`,\n );\n // Validate dimensions here, on the only path that reads it: a bad value\n // either throws RangeError in `new Array()` below or, past the serialization\n // budget MAX_EMBEDDING_DIMENSIONS encodes, kills the process outright. The\n // fixture-replay, chaos, strict and record/proxy branches above never touch\n // it, so they must not be gated.\n const dimensions = validateEmbeddingDimensions(embeddingReq.dimensions);\n if (dimensions === null) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: `Invalid parameter: 'dimensions' must be an integer between 1 and ${MAX_EMBEDDING_DIMENSIONS}`,\n type: \"invalid_request_error\",\n param: null,\n code: null,\n },\n }),\n );\n return;\n }\n\n const embeddings = inputs.map((input) => generateDeterministicEmbedding(input, dimensions));\n\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture: null },\n });\n\n const body = buildEmbeddingResponse(embeddings, embeddingReq.model);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"embeddings.cjs","names":["flattenHeaders","isJsonObject","normalizeEmbeddingInput","getContext","getTestId","selectFixtureForServing","applyChaosAsync","resolveResponse","isErrorResponse","serializeErrorResponse","isEmbeddingResponse","buildEmbeddingResponse","resolveStrictMode","strictNoMatchMessage","strictNoMatchLogLine","strictOverrideField","proxyAndRecord","validateEmbeddingDimensions","MAX_EMBEDDING_DIMENSIONS","generateDeterministicEmbedding"],"sources":["../src/embeddings.ts"],"sourcesContent":["/**\n * OpenAI Embeddings API support for aimock.\n *\n * Handles POST /v1/embeddings requests. Matches fixtures using the `inputText`\n * field, and falls back to generating a deterministic embedding from the input\n * text hash when no fixture matches.\n */\n\nimport type * as http from \"node:http\";\nimport type {\n ChatCompletionRequest,\n Fixture,\n HandlerDefaults,\n RecordProviderKey,\n} from \"./types.js\";\nimport {\n isEmbeddingResponse,\n isErrorResponse,\n serializeErrorResponse,\n generateDeterministicEmbedding,\n buildEmbeddingResponse,\n flattenHeaders,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n getContext,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n validateEmbeddingDimensions,\n isJsonObject,\n normalizeEmbeddingInput,\n MAX_EMBEDDING_DIMENSIONS,\n} from \"./helpers.js\";\nimport { releaseOneShotError } from \"./fixture-loader.js\";\nimport { selectFixtureForServing } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaosAsync } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\n// ─── Embeddings API request types ──────────────────────────────────────────\n\ninterface EmbeddingRequest {\n // Validated at runtime — these arrive as arbitrary JSON, so the declared\n // type must not claim a shape the parser cannot guarantee.\n input: unknown;\n model: string;\n encoding_format?: \"float\" | \"base64\";\n dimensions?: unknown;\n [key: string]: unknown;\n}\n\n// ─── Request handler ───────────────────────────────────────────────────────\n\nexport async function handleEmbeddings(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n providerKey: RecordProviderKey = \"openai\",\n // Native HTTP dispatch opts in; direct callers and compatible routes retain defaults.\n requireModel = false,\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let embeddingReq: EmbeddingRequest;\n try {\n embeddingReq = JSON.parse(raw) as EmbeddingRequest;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: `Malformed JSON body: ${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 `embeddingReq.input` throws a TypeError that\n // surfaces as a 500 instead of a 400.\n if (!isJsonObject(embeddingReq)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\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 // Validate required input parameter\n if (embeddingReq.input === undefined || embeddingReq.input === null) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: \"Missing required parameter: 'input'\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n if (requireModel && embeddingReq.model === undefined) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: \"Missing required parameter: 'model'\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n // Normalize input to array of strings — reject non-string inputs with 400\n // instead of crashing downstream in createHash().update() (500).\n const inputs = normalizeEmbeddingInput(embeddingReq.input);\n if (inputs === null) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message:\n \"Invalid parameter: 'input' must be a string, an array of strings, or an array of tokens\",\n type: \"invalid_request_error\",\n param: null,\n code: null,\n },\n }),\n );\n return;\n }\n\n // Concatenate all inputs for matching purposes\n const combinedInput = inputs.join(\" \");\n\n // Build a synthetic ChatCompletionRequest for the fixture router.\n // We attach `embeddingInput` so the router's inputText matching can use it.\n const syntheticReq: ChatCompletionRequest = {\n model: embeddingReq.model,\n messages: [],\n embeddingInput: combinedInput,\n _endpointType: \"embedding\",\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const { fixture, skippedBySequenceOrTurn } = selectFixtureForServing(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n logger.debug(`No fixture matched for request`);\n }\n\n if (\n await applyChaosAsync(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n {\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n ) {\n releaseOneShotError(fixtures, fixture);\n return;\n }\n\n if (fixture) {\n const response = await resolveResponse(fixture, syntheticReq);\n\n // Error response\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, serializeErrorResponse(response), {\n retryAfter: response.retryAfter,\n });\n return;\n }\n\n // Embedding response — use the fixture's embedding for each input\n if (isEmbeddingResponse(response)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n const embeddings = inputs.map(() => [...response.embedding]);\n const body = buildEmbeddingResponse(embeddings, embeddingReq.model);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n return;\n }\n\n // Fixture matched but response type is not compatible with embeddings\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message:\n \"Fixture response did not match any known embedding type (must have embedding or error)\",\n type: \"server_error\",\n },\n }),\n );\n return;\n }\n\n // No fixture match — check strict mode first, then try proxy\n if (resolveStrictMode(defaults.strict, req.headers)) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n logger.error(\n strictNoMatchLogLine(\n req.method ?? \"POST\",\n req.url ?? \"/v1/embeddings\",\n skippedBySequenceOrTurn,\n ),\n );\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\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 if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n providerKey,\n req.url ?? \"/v1/embeddings\",\n fixtures,\n defaults,\n raw,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome !== \"not_configured\") {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return;\n }\n }\n\n // No fixture match — generate deterministic embeddings from input text\n logger.warn(\n `No embedding fixture matched for \"${combinedInput.slice(0, 80)}\" — returning deterministic fallback`,\n );\n // Validate dimensions here, on the only path that reads it: a bad value\n // either throws RangeError in `new Array()` below or, past the serialization\n // budget MAX_EMBEDDING_DIMENSIONS encodes, kills the process outright. The\n // fixture-replay, chaos, strict and record/proxy branches above never touch\n // it, so they must not be gated.\n const dimensions = validateEmbeddingDimensions(embeddingReq.dimensions);\n if (dimensions === null) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: `Invalid parameter: 'dimensions' must be an integer between 1 and ${MAX_EMBEDDING_DIMENSIONS}`,\n type: \"invalid_request_error\",\n param: null,\n code: null,\n },\n }),\n );\n return;\n }\n\n const embeddings = inputs.map((input) => generateDeterministicEmbedding(input, dimensions));\n\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture: null },\n });\n\n const body = buildEmbeddingResponse(embeddings, embeddingReq.model);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n}\n"],"mappings":"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"}
@@ -4,7 +4,7 @@ import * as http$1 from "node:http";
4
4
 
5
5
  //#region src/embeddings.d.ts
6
6
 
7
- declare function handleEmbeddings(req: http$1.IncomingMessage, res: http$1.ServerResponse, raw: string, fixtures: Fixture[], journal: Journal, defaults: HandlerDefaults, setCorsHeaders: (res: http$1.ServerResponse) => void, providerKey?: RecordProviderKey): Promise<void>;
7
+ declare function handleEmbeddings(req: http$1.IncomingMessage, res: http$1.ServerResponse, raw: string, fixtures: Fixture[], journal: Journal, defaults: HandlerDefaults, setCorsHeaders: (res: http$1.ServerResponse) => void, providerKey?: RecordProviderKey, requireModel?: boolean): Promise<void>;
8
8
  //# sourceMappingURL=embeddings.d.ts.map
9
9
 
10
10
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"embeddings.d.cts","names":[],"sources":["../src/embeddings.ts"],"sourcesContent":[],"mappings":";;;;;;AA4DW,iBALW,gBAAA,CAKX,GAAA,EAJJ,MAAA,CAAK,eAID,EAAA,GAAA,EAHJ,MAAA,CAAK,cAGD,EAAA,GAAA,EAAA,MAAA,EAAA,QAAA,EADC,OACD,EAAA,EAAA,OAAA,EAAA,OAAA,EAAA,QAAA,EACC,eADD,EAAA,cAAA,EAAA,CAAA,GAAA,EAEa,MAAA,CAAK,cAFlB,EAAA,GAAA,IAAA,EAAA,WAAA,CAAA,EAGI,iBAHJ,CAAA,EAIR,OAJQ,CAAA,IAAA,CAAA"}
1
+ {"version":3,"file":"embeddings.d.cts","names":[],"sources":["../src/embeddings.ts"],"sourcesContent":[],"mappings":";;;;;;AA4DW,iBALW,gBAAA,CAKX,GAAA,EAJJ,MAAA,CAAK,eAID,EAAA,GAAA,EAHJ,MAAA,CAAK,cAGD,EAAA,GAAA,EAAA,MAAA,EAAA,QAAA,EADC,OACD,EAAA,EAAA,OAAA,EAAA,OAAA,EAAA,QAAA,EACC,eADD,EAAA,cAAA,EAAA,CAAA,GAAA,EAEa,MAAA,CAAK,cAFlB,EAAA,GAAA,IAAA,EAAA,WAAA,CAAA,EAGI,iBAHJ,EAAA,YAAA,CAAA,EAAA,OAAA,CAAA,EAMR,OANQ,CAAA,IAAA,CAAA"}
@@ -4,7 +4,7 @@ import * as http$1 from "node:http";
4
4
 
5
5
  //#region src/embeddings.d.ts
6
6
 
7
- declare function handleEmbeddings(req: http$1.IncomingMessage, res: http$1.ServerResponse, raw: string, fixtures: Fixture[], journal: Journal, defaults: HandlerDefaults, setCorsHeaders: (res: http$1.ServerResponse) => void, providerKey?: RecordProviderKey): Promise<void>;
7
+ declare function handleEmbeddings(req: http$1.IncomingMessage, res: http$1.ServerResponse, raw: string, fixtures: Fixture[], journal: Journal, defaults: HandlerDefaults, setCorsHeaders: (res: http$1.ServerResponse) => void, providerKey?: RecordProviderKey, requireModel?: boolean): Promise<void>;
8
8
  //# sourceMappingURL=embeddings.d.ts.map
9
9
 
10
10
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"embeddings.d.ts","names":[],"sources":["../src/embeddings.ts"],"sourcesContent":[],"mappings":";;;;;;AA4DW,iBALW,gBAAA,CAKX,GAAA,EAJJ,MAAA,CAAK,eAID,EAAA,GAAA,EAHJ,MAAA,CAAK,cAGD,EAAA,GAAA,EAAA,MAAA,EAAA,QAAA,EADC,OACD,EAAA,EAAA,OAAA,EAAA,OAAA,EAAA,QAAA,EACC,eADD,EAAA,cAAA,EAAA,CAAA,GAAA,EAEa,MAAA,CAAK,cAFlB,EAAA,GAAA,IAAA,EAAA,WAAA,CAAA,EAGI,iBAHJ,CAAA,EAIR,OAJQ,CAAA,IAAA,CAAA"}
1
+ {"version":3,"file":"embeddings.d.ts","names":[],"sources":["../src/embeddings.ts"],"sourcesContent":[],"mappings":";;;;;;AA4DW,iBALW,gBAAA,CAKX,GAAA,EAJJ,MAAA,CAAK,eAID,EAAA,GAAA,EAHJ,MAAA,CAAK,cAGD,EAAA,GAAA,EAAA,MAAA,EAAA,QAAA,EADC,OACD,EAAA,EAAA,OAAA,EAAA,OAAA,EAAA,QAAA,EACC,eADD,EAAA,cAAA,EAAA,CAAA,GAAA,EAEa,MAAA,CAAK,cAFlB,EAAA,GAAA,IAAA,EAAA,WAAA,CAAA,EAGI,iBAHJ,EAAA,YAAA,CAAA,EAAA,OAAA,CAAA,EAMR,OANQ,CAAA,IAAA,CAAA"}
@@ -6,7 +6,7 @@ import { selectFixtureForServing } from "./router.js";
6
6
  import { proxyAndRecord } from "./recorder.js";
7
7
 
8
8
  //#region src/embeddings.ts
9
- async function handleEmbeddings(req, res, raw, fixtures, journal, defaults, setCorsHeaders, providerKey = "openai") {
9
+ async function handleEmbeddings(req, res, raw, fixtures, journal, defaults, setCorsHeaders, providerKey = "openai", requireModel = false) {
10
10
  const { logger } = defaults;
11
11
  setCorsHeaders(res);
12
12
  let embeddingReq;
@@ -65,6 +65,23 @@ async function handleEmbeddings(req, res, raw, fixtures, journal, defaults, setC
65
65
  } }));
66
66
  return;
67
67
  }
68
+ if (requireModel && embeddingReq.model === void 0) {
69
+ journal.add({
70
+ method: req.method ?? "POST",
71
+ path: req.url ?? "/v1/embeddings",
72
+ headers: flattenHeaders(req.headers),
73
+ body: null,
74
+ response: {
75
+ status: 400,
76
+ fixture: null
77
+ }
78
+ });
79
+ writeErrorResponse(res, 400, JSON.stringify({ error: {
80
+ message: "Missing required parameter: 'model'",
81
+ type: "invalid_request_error"
82
+ } }));
83
+ return;
84
+ }
68
85
  const inputs = normalizeEmbeddingInput(embeddingReq.input);
69
86
  if (inputs === null) {
70
87
  journal.add({
@@ -1 +1 @@
1
- {"version":3,"file":"embeddings.js","names":[],"sources":["../src/embeddings.ts"],"sourcesContent":["/**\n * OpenAI Embeddings API support for aimock.\n *\n * Handles POST /v1/embeddings requests. Matches fixtures using the `inputText`\n * field, and falls back to generating a deterministic embedding from the input\n * text hash when no fixture matches.\n */\n\nimport type * as http from \"node:http\";\nimport type {\n ChatCompletionRequest,\n Fixture,\n HandlerDefaults,\n RecordProviderKey,\n} from \"./types.js\";\nimport {\n isEmbeddingResponse,\n isErrorResponse,\n serializeErrorResponse,\n generateDeterministicEmbedding,\n buildEmbeddingResponse,\n flattenHeaders,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n getContext,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n validateEmbeddingDimensions,\n isJsonObject,\n normalizeEmbeddingInput,\n MAX_EMBEDDING_DIMENSIONS,\n} from \"./helpers.js\";\nimport { releaseOneShotError } from \"./fixture-loader.js\";\nimport { selectFixtureForServing } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaosAsync } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\n// ─── Embeddings API request types ──────────────────────────────────────────\n\ninterface EmbeddingRequest {\n // Validated at runtime — these arrive as arbitrary JSON, so the declared\n // type must not claim a shape the parser cannot guarantee.\n input: unknown;\n model: string;\n encoding_format?: \"float\" | \"base64\";\n dimensions?: unknown;\n [key: string]: unknown;\n}\n\n// ─── Request handler ───────────────────────────────────────────────────────\n\nexport async function handleEmbeddings(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n providerKey: RecordProviderKey = \"openai\",\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let embeddingReq: EmbeddingRequest;\n try {\n embeddingReq = JSON.parse(raw) as EmbeddingRequest;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: `Malformed JSON body: ${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 `embeddingReq.input` throws a TypeError that\n // surfaces as a 500 instead of a 400.\n if (!isJsonObject(embeddingReq)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\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 // Validate required input parameter\n if (embeddingReq.input === undefined || embeddingReq.input === null) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: \"Missing required parameter: 'input'\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n // Normalize input to array of strings — reject non-string inputs with 400\n // instead of crashing downstream in createHash().update() (500).\n const inputs = normalizeEmbeddingInput(embeddingReq.input);\n if (inputs === null) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message:\n \"Invalid parameter: 'input' must be a string, an array of strings, or an array of tokens\",\n type: \"invalid_request_error\",\n param: null,\n code: null,\n },\n }),\n );\n return;\n }\n\n // Concatenate all inputs for matching purposes\n const combinedInput = inputs.join(\" \");\n\n // Build a synthetic ChatCompletionRequest for the fixture router.\n // We attach `embeddingInput` so the router's inputText matching can use it.\n const syntheticReq: ChatCompletionRequest = {\n model: embeddingReq.model,\n messages: [],\n embeddingInput: combinedInput,\n _endpointType: \"embedding\",\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const { fixture, skippedBySequenceOrTurn } = selectFixtureForServing(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n logger.debug(`No fixture matched for request`);\n }\n\n if (\n await applyChaosAsync(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n {\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n ) {\n releaseOneShotError(fixtures, fixture);\n return;\n }\n\n if (fixture) {\n const response = await resolveResponse(fixture, syntheticReq);\n\n // Error response\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, serializeErrorResponse(response), {\n retryAfter: response.retryAfter,\n });\n return;\n }\n\n // Embedding response — use the fixture's embedding for each input\n if (isEmbeddingResponse(response)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n const embeddings = inputs.map(() => [...response.embedding]);\n const body = buildEmbeddingResponse(embeddings, embeddingReq.model);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n return;\n }\n\n // Fixture matched but response type is not compatible with embeddings\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message:\n \"Fixture response did not match any known embedding type (must have embedding or error)\",\n type: \"server_error\",\n },\n }),\n );\n return;\n }\n\n // No fixture match — check strict mode first, then try proxy\n if (resolveStrictMode(defaults.strict, req.headers)) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n logger.error(\n strictNoMatchLogLine(\n req.method ?? \"POST\",\n req.url ?? \"/v1/embeddings\",\n skippedBySequenceOrTurn,\n ),\n );\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\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 if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n providerKey,\n req.url ?? \"/v1/embeddings\",\n fixtures,\n defaults,\n raw,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome !== \"not_configured\") {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return;\n }\n }\n\n // No fixture match — generate deterministic embeddings from input text\n logger.warn(\n `No embedding fixture matched for \"${combinedInput.slice(0, 80)}\" — returning deterministic fallback`,\n );\n // Validate dimensions here, on the only path that reads it: a bad value\n // either throws RangeError in `new Array()` below or, past the serialization\n // budget MAX_EMBEDDING_DIMENSIONS encodes, kills the process outright. The\n // fixture-replay, chaos, strict and record/proxy branches above never touch\n // it, so they must not be gated.\n const dimensions = validateEmbeddingDimensions(embeddingReq.dimensions);\n if (dimensions === null) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: `Invalid parameter: 'dimensions' must be an integer between 1 and ${MAX_EMBEDDING_DIMENSIONS}`,\n type: \"invalid_request_error\",\n param: null,\n code: null,\n },\n }),\n );\n return;\n }\n\n const embeddings = inputs.map((input) => generateDeterministicEmbedding(input, dimensions));\n\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture: null },\n });\n\n const body = buildEmbeddingResponse(embeddings, embeddingReq.model);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"embeddings.js","names":[],"sources":["../src/embeddings.ts"],"sourcesContent":["/**\n * OpenAI Embeddings API support for aimock.\n *\n * Handles POST /v1/embeddings requests. Matches fixtures using the `inputText`\n * field, and falls back to generating a deterministic embedding from the input\n * text hash when no fixture matches.\n */\n\nimport type * as http from \"node:http\";\nimport type {\n ChatCompletionRequest,\n Fixture,\n HandlerDefaults,\n RecordProviderKey,\n} from \"./types.js\";\nimport {\n isEmbeddingResponse,\n isErrorResponse,\n serializeErrorResponse,\n generateDeterministicEmbedding,\n buildEmbeddingResponse,\n flattenHeaders,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n getContext,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n validateEmbeddingDimensions,\n isJsonObject,\n normalizeEmbeddingInput,\n MAX_EMBEDDING_DIMENSIONS,\n} from \"./helpers.js\";\nimport { releaseOneShotError } from \"./fixture-loader.js\";\nimport { selectFixtureForServing } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaosAsync } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\n// ─── Embeddings API request types ──────────────────────────────────────────\n\ninterface EmbeddingRequest {\n // Validated at runtime — these arrive as arbitrary JSON, so the declared\n // type must not claim a shape the parser cannot guarantee.\n input: unknown;\n model: string;\n encoding_format?: \"float\" | \"base64\";\n dimensions?: unknown;\n [key: string]: unknown;\n}\n\n// ─── Request handler ───────────────────────────────────────────────────────\n\nexport async function handleEmbeddings(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n providerKey: RecordProviderKey = \"openai\",\n // Native HTTP dispatch opts in; direct callers and compatible routes retain defaults.\n requireModel = false,\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let embeddingReq: EmbeddingRequest;\n try {\n embeddingReq = JSON.parse(raw) as EmbeddingRequest;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: `Malformed JSON body: ${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 `embeddingReq.input` throws a TypeError that\n // surfaces as a 500 instead of a 400.\n if (!isJsonObject(embeddingReq)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\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 // Validate required input parameter\n if (embeddingReq.input === undefined || embeddingReq.input === null) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: \"Missing required parameter: 'input'\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n if (requireModel && embeddingReq.model === undefined) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: \"Missing required parameter: 'model'\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n // Normalize input to array of strings — reject non-string inputs with 400\n // instead of crashing downstream in createHash().update() (500).\n const inputs = normalizeEmbeddingInput(embeddingReq.input);\n if (inputs === null) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message:\n \"Invalid parameter: 'input' must be a string, an array of strings, or an array of tokens\",\n type: \"invalid_request_error\",\n param: null,\n code: null,\n },\n }),\n );\n return;\n }\n\n // Concatenate all inputs for matching purposes\n const combinedInput = inputs.join(\" \");\n\n // Build a synthetic ChatCompletionRequest for the fixture router.\n // We attach `embeddingInput` so the router's inputText matching can use it.\n const syntheticReq: ChatCompletionRequest = {\n model: embeddingReq.model,\n messages: [],\n embeddingInput: combinedInput,\n _endpointType: \"embedding\",\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const { fixture, skippedBySequenceOrTurn } = selectFixtureForServing(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n logger.debug(`No fixture matched for request`);\n }\n\n if (\n await applyChaosAsync(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n {\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n ) {\n releaseOneShotError(fixtures, fixture);\n return;\n }\n\n if (fixture) {\n const response = await resolveResponse(fixture, syntheticReq);\n\n // Error response\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, serializeErrorResponse(response), {\n retryAfter: response.retryAfter,\n });\n return;\n }\n\n // Embedding response — use the fixture's embedding for each input\n if (isEmbeddingResponse(response)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n const embeddings = inputs.map(() => [...response.embedding]);\n const body = buildEmbeddingResponse(embeddings, embeddingReq.model);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n return;\n }\n\n // Fixture matched but response type is not compatible with embeddings\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message:\n \"Fixture response did not match any known embedding type (must have embedding or error)\",\n type: \"server_error\",\n },\n }),\n );\n return;\n }\n\n // No fixture match — check strict mode first, then try proxy\n if (resolveStrictMode(defaults.strict, req.headers)) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n logger.error(\n strictNoMatchLogLine(\n req.method ?? \"POST\",\n req.url ?? \"/v1/embeddings\",\n skippedBySequenceOrTurn,\n ),\n );\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\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 if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n providerKey,\n req.url ?? \"/v1/embeddings\",\n fixtures,\n defaults,\n raw,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome !== \"not_configured\") {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return;\n }\n }\n\n // No fixture match — generate deterministic embeddings from input text\n logger.warn(\n `No embedding fixture matched for \"${combinedInput.slice(0, 80)}\" — returning deterministic fallback`,\n );\n // Validate dimensions here, on the only path that reads it: a bad value\n // either throws RangeError in `new Array()` below or, past the serialization\n // budget MAX_EMBEDDING_DIMENSIONS encodes, kills the process outright. The\n // fixture-replay, chaos, strict and record/proxy branches above never touch\n // it, so they must not be gated.\n const dimensions = validateEmbeddingDimensions(embeddingReq.dimensions);\n if (dimensions === null) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: `Invalid parameter: 'dimensions' must be an integer between 1 and ${MAX_EMBEDDING_DIMENSIONS}`,\n type: \"invalid_request_error\",\n param: null,\n code: null,\n },\n }),\n );\n return;\n }\n\n const embeddings = inputs.map((input) => generateDeterministicEmbedding(input, dimensions));\n\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture: null },\n });\n\n const body = buildEmbeddingResponse(embeddings, embeddingReq.model);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n}\n"],"mappings":"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"}
@@ -0,0 +1,131 @@
1
+ const require_mcp_fakes = require('./mcp-fakes.cjs');
2
+ const require_mcp_fakes_report = require('./mcp-fakes-report.cjs');
3
+
4
+ //#region src/fakes-plugin.ts
5
+ /** TI1/TI2 separator: space, U+203A, space. */
6
+ const TEST_ID_SEPARATOR = " › ";
7
+ const FAKES_REPORT_MODES = [
8
+ "off",
9
+ "warn",
10
+ "fail"
11
+ ];
12
+ /** The `fakesReport` option, checked: an unknown mode fails `beforeAll`. */
13
+ function fakesReportMode(mode) {
14
+ if (mode === void 0) return "off";
15
+ if (!FAKES_REPORT_MODES.includes(mode)) throw new Error(`useAimock(): fakesReport must be one of ${FAKES_REPORT_MODES.map((m) => JSON.stringify(m)).join(", ")}; got ${JSON.stringify(mode)}`);
16
+ return mode;
17
+ }
18
+ /**
19
+ * The URL and headers that reach the fakes scoped to `testId` (and
20
+ * `opts.context`) at `opts.mount`. `URLSearchParams` writes a space as `+`,
21
+ * which the mount reads back as a space in the query.
22
+ */
23
+ function fakesTarget(url, testId, opts = {}) {
24
+ const mount = opts.mount ?? "/mcp";
25
+ const query = new URLSearchParams({ testId });
26
+ const headers = { "X-Test-Id": encodeURIComponent(testId) };
27
+ if (opts.context) {
28
+ query.set("context", opts.context);
29
+ headers["X-AIMock-Context"] = encodeURIComponent(opts.context);
30
+ }
31
+ return {
32
+ testId,
33
+ mcpUrl: `${url}${mount}?${query.toString()}`,
34
+ headers
35
+ };
36
+ }
37
+ /** RP6: read `GET /__aimock/mcp/fakes/report` for one identity. */
38
+ async function fetchFakesReport(url, testId, context) {
39
+ const query = new URLSearchParams();
40
+ if (testId !== null) query.set("testId", testId);
41
+ if (context !== null) query.set("context", context);
42
+ const res = await fetch(`${url}/__aimock/mcp/fakes/report?${query.toString()}`);
43
+ if (!res.ok) throw new Error(`aimock fakes report: HTTP ${res.status} ${await res.text()}`);
44
+ return await res.json();
45
+ }
46
+ function identityKey(testId, context) {
47
+ return `${testId ?? ""}\u0000${context ?? ""}`;
48
+ }
49
+ /** Per-`useAimock` state shared by the Vitest and Jest adapters. */
50
+ var FakesPluginState = class {
51
+ registered = /* @__PURE__ */ new Map();
52
+ /** RP7 (b): id of the last journal entry when the test started (C2). */
53
+ marker = null;
54
+ constructor(mode) {
55
+ this.mode = mode;
56
+ }
57
+ /** Start of a test, after the match-count reset. */
58
+ beforeEach(llm) {
59
+ this.registered.clear();
60
+ this.marker = llm.getLastRequest()?.id ?? null;
61
+ }
62
+ /** RP7 (a): `fakesFor` registers the identity for the current test. */
63
+ register(testId, context) {
64
+ this.registered.set(identityKey(testId, context), {
65
+ testId,
66
+ context
67
+ });
68
+ }
69
+ /**
70
+ * RP7 (a) and (b): the registered identities, plus every (test id, context)
71
+ * an MCP request carried since the marker. If the marker entry is gone
72
+ * (FIFO cap or a cleared journal), every remaining entry is newer.
73
+ */
74
+ identities(llm) {
75
+ const all = llm.getRequests();
76
+ const at = this.marker === null ? -1 : all.findIndex((e) => e.id === this.marker);
77
+ const newer = at >= 0 ? all.slice(at + 1) : all;
78
+ const out = new Map(this.registered);
79
+ for (const entry of newer) {
80
+ if (entry.service !== "mcp" || entry.testId === void 0) continue;
81
+ const testId = entry.testId === require_mcp_fakes.MCP_FAKES_DEFAULT_TEST_ID ? null : entry.testId;
82
+ const context = entry.context ?? null;
83
+ out.set(identityKey(testId, context), {
84
+ testId,
85
+ context
86
+ });
87
+ }
88
+ return [...out.values()];
89
+ }
90
+ /**
91
+ * End of a test, before the next reset: read each identity's report.
92
+ * Under `"fail"`, a report with `ok: false` throws `AimockFakesReportError`
93
+ * (the first failing report, with every failing report's text); under
94
+ * `"warn"`, the text goes to `console.warn`. An unconsumed entry two
95
+ * reports share (an entry scoped by test id only, checked with and without
96
+ * a context) is listed once.
97
+ */
98
+ async afterEach(llm, url) {
99
+ if (this.mode === "off") return;
100
+ const failing = [];
101
+ const lines = [];
102
+ const unconsumedSeen = /* @__PURE__ */ new Set();
103
+ for (const id of this.identities(llm)) {
104
+ const report = await fetchFakesReport(url, id.testId, id.context);
105
+ if (report.ok) continue;
106
+ failing.push(report);
107
+ for (const line of require_mcp_fakes_report.formatFakesReport(report).split("\n")) {
108
+ if (line.startsWith(" unconsumed: ")) {
109
+ if (unconsumedSeen.has(line)) continue;
110
+ unconsumedSeen.add(line);
111
+ }
112
+ lines.push(line);
113
+ }
114
+ }
115
+ if (failing.length === 0) return;
116
+ const text = lines.join("\n");
117
+ if (this.mode === "warn") {
118
+ console.warn(text);
119
+ return;
120
+ }
121
+ throw new require_mcp_fakes_report.AimockFakesReportError(failing[0], text);
122
+ }
123
+ };
124
+
125
+ //#endregion
126
+ exports.FakesPluginState = FakesPluginState;
127
+ exports.TEST_ID_SEPARATOR = TEST_ID_SEPARATOR;
128
+ exports.fakesReportMode = fakesReportMode;
129
+ exports.fakesTarget = fakesTarget;
130
+ exports.fetchFakesReport = fetchFakesReport;
131
+ //# sourceMappingURL=fakes-plugin.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fakes-plugin.cjs","names":["MCP_FAKES_DEFAULT_TEST_ID","formatFakesReport","AimockFakesReportError"],"sources":["../src/fakes-plugin.ts"],"sourcesContent":["/**\n * The MCP fakes part of the Vitest and Jest plugins (record-replay RP7, TI):\n * `fakesFor` targets, the per-test fake report check in `afterEach`, and the\n * state both adapters share. The default test id itself is built by each\n * adapter (TI1 Vitest, TI2 Jest), and it is used only by the fakes helpers\n * (TI4): nothing here tags LLM or control traffic.\n */\nimport type { LLMock } from \"./llmock.js\";\nimport {\n AimockFakesReportError,\n formatFakesReport,\n type McpFakesReport,\n} from \"./mcp-fakes-report.js\";\nimport { MCP_FAKES_DEFAULT_TEST_ID } from \"./mcp-fakes.js\";\n\n/** RP7: what `afterEach` does with a failing report. `\"off\"` is the default. */\nexport type FakesReportMode = \"off\" | \"warn\" | \"fail\";\n\n/** RP7: what `fakesFor` returns, for an MCP client. */\nexport interface FakesTarget {\n /** The test id the URL and headers carry (explicit, or the inferred default). */\n testId: string;\n /** The mount URL with `?testId=` (and `&context=`). */\n mcpUrl: string;\n /** The matching `X-Test-Id` / `X-AIMock-Context` headers, URI-encoded. */\n headers: Record<string, string>;\n}\n\nexport interface FakesForOptions {\n /** The context the fake blocks are scoped to. */\n context?: string;\n /** The MCP mount path (default `\"/mcp\"`). */\n mount?: string;\n}\n\n/** TI1/TI2 separator: space, U+203A, space. */\nexport const TEST_ID_SEPARATOR = \" › \";\n\nconst FAKES_REPORT_MODES: readonly FakesReportMode[] = [\"off\", \"warn\", \"fail\"];\n\n/** The `fakesReport` option, checked: an unknown mode fails `beforeAll`. */\nexport function fakesReportMode(mode: FakesReportMode | undefined): FakesReportMode {\n if (mode === undefined) return \"off\";\n if (!FAKES_REPORT_MODES.includes(mode)) {\n throw new Error(\n `useAimock(): fakesReport must be one of ${FAKES_REPORT_MODES.map((m) => JSON.stringify(m)).join(\", \")}; got ${JSON.stringify(mode)}`,\n );\n }\n return mode;\n}\n\n/**\n * The URL and headers that reach the fakes scoped to `testId` (and\n * `opts.context`) at `opts.mount`. `URLSearchParams` writes a space as `+`,\n * which the mount reads back as a space in the query.\n */\nexport function fakesTarget(url: string, testId: string, opts: FakesForOptions = {}): FakesTarget {\n const mount = opts.mount ?? \"/mcp\";\n const query = new URLSearchParams({ testId });\n const headers: Record<string, string> = { \"X-Test-Id\": encodeURIComponent(testId) };\n if (opts.context) {\n query.set(\"context\", opts.context);\n headers[\"X-AIMock-Context\"] = encodeURIComponent(opts.context);\n }\n return { testId, mcpUrl: `${url}${mount}?${query.toString()}`, headers };\n}\n\n/** RP6: read `GET /__aimock/mcp/fakes/report` for one identity. */\nexport async function fetchFakesReport(\n url: string,\n testId: string | null,\n context: string | null,\n): Promise<McpFakesReport> {\n const query = new URLSearchParams();\n if (testId !== null) query.set(\"testId\", testId);\n if (context !== null) query.set(\"context\", context);\n const res = await fetch(`${url}/__aimock/mcp/fakes/report?${query.toString()}`);\n if (!res.ok) {\n throw new Error(`aimock fakes report: HTTP ${res.status} ${await res.text()}`);\n }\n return (await res.json()) as McpFakesReport;\n}\n\n/** One identity the report checks; `null` means none supplied. */\nexport interface FakesIdentity {\n testId: string | null;\n context: string | null;\n}\n\nfunction identityKey(testId: string | null, context: string | null): string {\n return `${testId ?? \"\"}\\u0000${context ?? \"\"}`;\n}\n\n/** Per-`useAimock` state shared by the Vitest and Jest adapters. */\nexport class FakesPluginState {\n private readonly registered = new Map<string, FakesIdentity>();\n /** RP7 (b): id of the last journal entry when the test started (C2). */\n private marker: string | null = null;\n\n constructor(readonly mode: FakesReportMode) {}\n\n /** Start of a test, after the match-count reset. */\n beforeEach(llm: LLMock): void {\n this.registered.clear();\n this.marker = llm.getLastRequest()?.id ?? null;\n }\n\n /** RP7 (a): `fakesFor` registers the identity for the current test. */\n register(testId: string, context: string | null): void {\n this.registered.set(identityKey(testId, context), { testId, context });\n }\n\n /**\n * RP7 (a) and (b): the registered identities, plus every (test id, context)\n * an MCP request carried since the marker. If the marker entry is gone\n * (FIFO cap or a cleared journal), every remaining entry is newer.\n */\n identities(llm: LLMock): FakesIdentity[] {\n const all = llm.getRequests();\n const at = this.marker === null ? -1 : all.findIndex((e) => e.id === this.marker);\n const newer = at >= 0 ? all.slice(at + 1) : all;\n const out = new Map(this.registered);\n for (const entry of newer) {\n if (entry.service !== \"mcp\" || entry.testId === undefined) continue;\n const testId = entry.testId === MCP_FAKES_DEFAULT_TEST_ID ? null : entry.testId;\n const context = entry.context ?? null;\n out.set(identityKey(testId, context), { testId, context });\n }\n return [...out.values()];\n }\n\n /**\n * End of a test, before the next reset: read each identity's report.\n * Under `\"fail\"`, a report with `ok: false` throws `AimockFakesReportError`\n * (the first failing report, with every failing report's text); under\n * `\"warn\"`, the text goes to `console.warn`. An unconsumed entry two\n * reports share (an entry scoped by test id only, checked with and without\n * a context) is listed once.\n */\n async afterEach(llm: LLMock, url: string): Promise<void> {\n if (this.mode === \"off\") return;\n const failing: McpFakesReport[] = [];\n const lines: string[] = [];\n const unconsumedSeen = new Set<string>();\n for (const id of this.identities(llm)) {\n const report = await fetchFakesReport(url, id.testId, id.context);\n if (report.ok) continue;\n failing.push(report);\n for (const line of formatFakesReport(report).split(\"\\n\")) {\n if (line.startsWith(\" unconsumed: \")) {\n if (unconsumedSeen.has(line)) continue;\n unconsumedSeen.add(line);\n }\n lines.push(line);\n }\n }\n if (failing.length === 0) return;\n const text = lines.join(\"\\n\");\n if (this.mode === \"warn\") {\n console.warn(text);\n return;\n }\n throw new AimockFakesReportError(failing[0], text);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ //#region src/fakes-plugin.d.ts
2
+
3
+ /** RP7: what `afterEach` does with a failing report. `"off"` is the default. */
4
+ type FakesReportMode = "off" | "warn" | "fail";
5
+ /** RP7: what `fakesFor` returns, for an MCP client. */
6
+ interface FakesTarget {
7
+ /** The test id the URL and headers carry (explicit, or the inferred default). */
8
+ testId: string;
9
+ /** The mount URL with `?testId=` (and `&context=`). */
10
+ mcpUrl: string;
11
+ /** The matching `X-Test-Id` / `X-AIMock-Context` headers, URI-encoded. */
12
+ headers: Record<string, string>;
13
+ }
14
+ interface FakesForOptions {
15
+ /** The context the fake blocks are scoped to. */
16
+ context?: string;
17
+ /** The MCP mount path (default `"/mcp"`). */
18
+ mount?: string;
19
+ }
20
+ /** TI1/TI2 separator: space, U+203A, space. */
21
+ //#endregion
22
+ export { FakesForOptions, FakesReportMode, FakesTarget };
23
+ //# sourceMappingURL=fakes-plugin.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fakes-plugin.d.cts","names":[],"sources":["../src/fakes-plugin.ts"],"sourcesContent":[],"mappings":";;;KAgBY,eAAA;;UAGK,WAAA;;;;;;WAMN;;UAGM,eAAA"}
@@ -0,0 +1,23 @@
1
+ //#region src/fakes-plugin.d.ts
2
+
3
+ /** RP7: what `afterEach` does with a failing report. `"off"` is the default. */
4
+ type FakesReportMode = "off" | "warn" | "fail";
5
+ /** RP7: what `fakesFor` returns, for an MCP client. */
6
+ interface FakesTarget {
7
+ /** The test id the URL and headers carry (explicit, or the inferred default). */
8
+ testId: string;
9
+ /** The mount URL with `?testId=` (and `&context=`). */
10
+ mcpUrl: string;
11
+ /** The matching `X-Test-Id` / `X-AIMock-Context` headers, URI-encoded. */
12
+ headers: Record<string, string>;
13
+ }
14
+ interface FakesForOptions {
15
+ /** The context the fake blocks are scoped to. */
16
+ context?: string;
17
+ /** The MCP mount path (default `"/mcp"`). */
18
+ mount?: string;
19
+ }
20
+ /** TI1/TI2 separator: space, U+203A, space. */
21
+ //#endregion
22
+ export { FakesForOptions, FakesReportMode, FakesTarget };
23
+ //# sourceMappingURL=fakes-plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fakes-plugin.d.ts","names":[],"sources":["../src/fakes-plugin.ts"],"sourcesContent":[],"mappings":";;;KAgBY,eAAA;;UAGK,WAAA;;;;;;WAMN;;UAGM,eAAA"}