@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
@@ -1,4 +1,4 @@
1
- import { flattenHeaders, generateDeterministicEmbedding, getContext, getTestId, isEmbeddingResponse, isErrorResponse, isJsonObject, resolveResponse, resolveStrictMode, strictNoMatchLogLine, strictNoMatchMessage, strictOverrideField } from "./helpers.js";
1
+ import { MAX_EMBEDDING_DIMENSIONS, flattenHeaders, generateDeterministicEmbedding, getContext, getTestId, isEmbeddingResponse, isErrorResponse, isJsonObject, resolveResponse, resolveStrictMode, strictNoMatchLogLine, strictNoMatchMessage, strictOverrideField } from "./helpers.js";
2
2
  import { writeErrorResponse } from "./sse-writer.js";
3
3
  import { applyChaosAsync } from "./chaos.js";
4
4
  import { matchFixtureDiagnostic } from "./router.js";
@@ -49,7 +49,27 @@ async function handleGeminiEmbedContent(req, res, raw, model, fixtures, journal,
49
49
  } }));
50
50
  return;
51
51
  }
52
- const inputText = (embedReq.content?.parts ?? []).filter((p) => p.text !== void 0).map((p) => p.text).join(" ");
52
+ const parts = embedReq.content?.parts ?? [];
53
+ const partsError = !Array.isArray(parts) ? "content.parts must be an array" : parts.some((part) => part === null) ? "content.parts must not contain null entries" : void 0;
54
+ if (partsError) {
55
+ journal.add({
56
+ method: req.method ?? "POST",
57
+ path: req.url ?? `/v1beta/models/${model}:embedContent`,
58
+ headers: flattenHeaders(req.headers),
59
+ body: null,
60
+ response: {
61
+ status: 400,
62
+ fixture: null
63
+ }
64
+ });
65
+ writeErrorResponse(res, 400, JSON.stringify({ error: {
66
+ message: partsError,
67
+ code: 400,
68
+ status: "INVALID_ARGUMENT"
69
+ } }));
70
+ return;
71
+ }
72
+ const inputText = parts.filter((p) => p.text !== void 0).map((p) => p.text).join(" ");
53
73
  const syntheticReq = {
54
74
  model: embedReq.model ?? model,
55
75
  messages: [],
@@ -164,7 +184,27 @@ async function handleGeminiEmbedContent(req, res, raw, model, fixtures, journal,
164
184
  return;
165
185
  }
166
186
  }
167
- const embedding = generateDeterministicEmbedding(inputText, embedReq.outputDimensionality ?? DEFAULT_GEMINI_EMBEDDING_DIMENSIONS);
187
+ const dimensions = embedReq.outputDimensionality ?? DEFAULT_GEMINI_EMBEDDING_DIMENSIONS;
188
+ const exceedsFallbackBudget = dimensions > MAX_EMBEDDING_DIMENSIONS;
189
+ if (exceedsFallbackBudget || typeof dimensions === "number" && (!Number.isInteger(dimensions) || dimensions < 0) || typeof dimensions === "string" && Number.isNaN(Number(dimensions))) {
190
+ journal.add({
191
+ method: req.method ?? "POST",
192
+ path,
193
+ headers: flattenHeaders(req.headers),
194
+ body: syntheticReq,
195
+ response: {
196
+ status: 400,
197
+ fixture: null
198
+ }
199
+ });
200
+ writeErrorResponse(res, 400, JSON.stringify({ error: {
201
+ message: exceedsFallbackBudget ? `outputDimensionality exceeds the fallback serialization budget of ${MAX_EMBEDDING_DIMENSIONS}` : "outputDimensionality cannot produce a numeric embedding",
202
+ code: 400,
203
+ status: "INVALID_ARGUMENT"
204
+ } }));
205
+ return;
206
+ }
207
+ const embedding = generateDeterministicEmbedding(inputText, dimensions);
168
208
  logger.warn(`No embedding fixture matched for "${inputText.slice(0, 80)}" — returning deterministic fallback`);
169
209
  journal.add({
170
210
  method: req.method ?? "POST",
@@ -1 +1 @@
1
- {"version":3,"file":"gemini-embeddings.js","names":[],"sources":["../src/gemini-embeddings.ts"],"sourcesContent":["/**\n * Google Gemini embedContent API support.\n *\n * Handles POST /v1beta/models/{model}:embedContent requests. Translates\n * incoming Gemini embedding requests into the ChatCompletionRequest format\n * used by the fixture router (with _endpointType: \"embedding\"), and converts\n * fixture responses back into the Gemini embedContent response format.\n *\n * Falls back to generating a deterministic embedding from the input text hash\n * when no fixture matches — same strategy as the OpenAI embeddings handler.\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 generateDeterministicEmbedding,\n flattenHeaders,\n isJsonObject,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n getContext,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { matchFixtureDiagnostic } 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// ─── Gemini embedContent request types ────────────────────────────────────\n\ninterface GeminiEmbedContentPart {\n text?: string;\n}\n\ninterface GeminiEmbedContentRequest {\n content?: { parts?: GeminiEmbedContentPart[] };\n model?: string;\n taskType?: string;\n title?: string;\n outputDimensionality?: number;\n [key: string]: unknown;\n}\n\n// ─── Gemini embedContent response type ────────────────────────────────────\n\ninterface GeminiEmbedContentResponse {\n embedding: {\n values: number[];\n };\n}\n\n// ─── Default embedding dimensions for Gemini ──────────────────────────────\n\nconst DEFAULT_GEMINI_EMBEDDING_DIMENSIONS = 768;\n\n// ─── Request handler ───────────────────────────────────────────────────────\n\nexport async function handleGeminiEmbedContent(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n model: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n providerKey: RecordProviderKey = \"gemini\",\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let embedReq: GeminiEmbedContentRequest;\n try {\n embedReq = JSON.parse(raw) as GeminiEmbedContentRequest;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? `/v1beta/models/${model}:embedContent`,\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 code: 400,\n status: \"INVALID_ARGUMENT\",\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 `embedReq.content` throws a TypeError that\n // surfaces as a 500 instead of a 400.\n if (!isJsonObject(embedReq)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? `/v1beta/models/${model}:embedContent`,\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 code: 400,\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n\n // Extract text from content.parts\n const parts = embedReq.content?.parts ?? [];\n const inputText = parts\n .filter((p) => p.text !== undefined)\n .map((p) => p.text!)\n .join(\" \");\n\n // Build a synthetic ChatCompletionRequest for the fixture router.\n // Uses _endpointType: \"embedding\" and embeddingInput to share fixtures\n // with OpenAI embeddings.\n const syntheticReq: ChatCompletionRequest = {\n model: embedReq.model ?? model,\n messages: [],\n embeddingInput: inputText,\n _endpointType: \"embedding\",\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n const path = req.url ?? `/v1beta/models/${model}:embedContent`;\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,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\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,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n const geminiError = {\n error: {\n code: status,\n message: response.error.message,\n status: response.error.type ?? \"ERROR\",\n },\n };\n writeErrorResponse(res, status, JSON.stringify(geminiError), {\n retryAfter: response.retryAfter,\n });\n return;\n }\n\n // Embedding response — use the fixture's embedding values\n if (isEmbeddingResponse(response)) {\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n const body: GeminiEmbedContentResponse = {\n embedding: {\n values: [...response.embedding],\n },\n };\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,\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 code: 500,\n status: \"INTERNAL\",\n },\n }),\n );\n return;\n }\n\n // No fixture match — check strict mode first, then try proxy\n const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n if (effectiveStrict) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n logger.error(strictNoMatchLogLine(req.method ?? \"POST\", path, skippedBySequenceOrTurn));\n journal.add({\n method: req.method ?? \"POST\",\n path,\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 code: 503,\n status: \"UNAVAILABLE\",\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 path,\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,\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 embedding from input text\n const dimensions = embedReq.outputDimensionality ?? DEFAULT_GEMINI_EMBEDDING_DIMENSIONS;\n const embedding = generateDeterministicEmbedding(inputText, dimensions);\n\n logger.warn(\n `No embedding fixture matched for \"${inputText.slice(0, 80)}\" — returning deterministic fallback`,\n );\n\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture: null },\n });\n\n const body: GeminiEmbedContentResponse = {\n embedding: {\n values: embedding,\n },\n };\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n}\n"],"mappings":";;;;;;;AAgEA,MAAM,sCAAsC;AAI5C,eAAsB,yBACpB,KACA,KACA,KACA,OACA,UACA,SACA,UACA,gBACA,cAAiC,UAClB;CACf,MAAM,EAAE,WAAW;AACnB,gBAAe,IAAI;CAEnB,IAAI;AACJ,KAAI;AACF,aAAW,KAAK,MAAM,IAAI;UACnB,UAAU;EACjB,MAAM,SAAS,oBAAoB,QAAQ,SAAS,UAAU;AAC9D,UAAQ,IAAI;GACV,QAAQ,IAAI,UAAU;GACtB,MAAM,IAAI,OAAO,kBAAkB,MAAM;GACzC,SAAS,eAAe,IAAI,QAAQ;GACpC,MAAM;GACN,UAAU;IAAE,QAAQ;IAAK,SAAS;IAAM;GACzC,CAAC;AACF,qBACE,KACA,KACA,KAAK,UAAU,EACb,OAAO;GACL,SAAS,wBAAwB;GACjC,MAAM;GACN,QAAQ;GACT,EACF,CAAC,CACH;AACD;;AAMF,KAAI,CAAC,aAAa,SAAS,EAAE;AAC3B,UAAQ,IAAI;GACV,QAAQ,IAAI,UAAU;GACtB,MAAM,IAAI,OAAO,kBAAkB,MAAM;GACzC,SAAS,eAAe,IAAI,QAAQ;GACpC,MAAM;GACN,UAAU;IAAE,QAAQ;IAAK,SAAS;IAAM;GACzC,CAAC;AACF,qBACE,KACA,KACA,KAAK,UAAU,EACb,OAAO;GACL,SAAS;GACT,MAAM;GACN,QAAQ;GACT,EACF,CAAC,CACH;AACD;;CAKF,MAAM,aADQ,SAAS,SAAS,SAAS,EAAE,EAExC,QAAQ,MAAM,EAAE,SAAS,OAAU,CACnC,KAAK,MAAM,EAAE,KAAM,CACnB,KAAK,IAAI;CAKZ,MAAM,eAAsC;EAC1C,OAAO,SAAS,SAAS;EACzB,UAAU,EAAE;EACZ,gBAAgB;EAChB,eAAe;EACf,UAAU,WAAW,IAAI;EAC1B;CAED,MAAM,SAAS,UAAU,IAAI;CAC7B,MAAM,EAAE,SAAS,4BAA4B,uBAC3C,UACA,cACA,QAAQ,6BAA6B,OAAO,EAC5C,SAAS,iBACV;CACD,MAAM,OAAO,IAAI,OAAO,kBAAkB,MAAM;AAEhD,KAAI,SAAS;AACX,UAAQ,2BAA2B,SAAS,UAAU,OAAO;AAC7D,SAAO,MAAM,oBAAoB,KAAK,UAAU,QAAQ,MAAM,CAAC,MAAM,GAAG,IAAI,GAAG;OAE/E,QAAO,MAAM,iCAAiC;AAGhD,KACE,MAAM,gBACJ,KACA,SACA,SAAS,OACT,IAAI,SACJ,IAAI,KACJ,SACA;EACE,QAAQ,IAAI,UAAU;EACtB;EACA,SAAS,eAAe,IAAI,QAAQ;EACpC,MAAM;EACP,EACD,UAAU,YAAY,SACtB,SAAS,UACT,SAAS,OACV,CAED;AAEF,KAAI,SAAS;EACX,MAAM,WAAW,MAAM,gBAAgB,SAAS,aAAa;AAG7D,MAAI,gBAAgB,SAAS,EAAE;GAC7B,MAAM,SAAS,SAAS,UAAU;AAClC,WAAQ,IAAI;IACV,QAAQ,IAAI,UAAU;IACtB;IACA,SAAS,eAAe,IAAI,QAAQ;IACpC,MAAM;IACN,UAAU;KAAE;KAAQ;KAAS;IAC9B,CAAC;GACF,MAAM,cAAc,EAClB,OAAO;IACL,MAAM;IACN,SAAS,SAAS,MAAM;IACxB,QAAQ,SAAS,MAAM,QAAQ;IAChC,EACF;AACD,sBAAmB,KAAK,QAAQ,KAAK,UAAU,YAAY,EAAE,EAC3D,YAAY,SAAS,YACtB,CAAC;AACF;;AAIF,MAAI,oBAAoB,SAAS,EAAE;AACjC,WAAQ,IAAI;IACV,QAAQ,IAAI,UAAU;IACtB;IACA,SAAS,eAAe,IAAI,QAAQ;IACpC,MAAM;IACN,UAAU;KAAE,QAAQ;KAAK;KAAS;IACnC,CAAC;GACF,MAAM,OAAmC,EACvC,WAAW,EACT,QAAQ,CAAC,GAAG,SAAS,UAAU,EAChC,EACF;AACD,OAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,OAAI,IAAI,KAAK,UAAU,KAAK,CAAC;AAC7B;;AAIF,UAAQ,IAAI;GACV,QAAQ,IAAI,UAAU;GACtB;GACA,SAAS,eAAe,IAAI,QAAQ;GACpC,MAAM;GACN,UAAU;IAAE,QAAQ;IAAK;IAAS;GACnC,CAAC;AACF,qBACE,KACA,KACA,KAAK,UAAU,EACb,OAAO;GACL,SACE;GACF,MAAM;GACN,QAAQ;GACT,EACF,CAAC,CACH;AACD;;AAKF,KADwB,kBAAkB,SAAS,QAAQ,IAAI,QAAQ,EAClD;EACnB,MAAM,gBAAgB,qBAAqB,wBAAwB;AACnE,SAAO,MAAM,qBAAqB,IAAI,UAAU,QAAQ,MAAM,wBAAwB,CAAC;AACvF,UAAQ,IAAI;GACV,QAAQ,IAAI,UAAU;GACtB;GACA,SAAS,eAAe,IAAI,QAAQ;GACpC,MAAM;GACN,UAAU;IACR,QAAQ;IACR,SAAS;IACT,GAAG,oBAAoB,SAAS,QAAQ,IAAI,QAAQ;IACrD;GACF,CAAC;AACF,qBACE,KACA,KACA,KAAK,UAAU,EACb,OAAO;GACL,SAAS;GACT,MAAM;GACN,QAAQ;GACT,EACF,CAAC,CACH;AACD;;AAGF,KAAI,SAAS,QAAQ;EACnB,MAAM,UAAU,MAAM,eACpB,KACA,KACA,cACA,aACA,MACA,UACA,UACA,IACD;AACD,MAAI,YAAY,kBAAmB;AACnC,MAAI,YAAY,kBAAkB;AAChC,WAAQ,IAAI;IACV,QAAQ,IAAI,UAAU;IACtB;IACA,SAAS,eAAe,IAAI,QAAQ;IACpC,MAAM;IACN,UAAU;KAAE,QAAQ,IAAI,cAAc;KAAK,SAAS;KAAM,QAAQ;KAAS;IAC5E,CAAC;AACF;;;CAMJ,MAAM,YAAY,+BAA+B,WAD9B,SAAS,wBAAwB,oCACmB;AAEvE,QAAO,KACL,qCAAqC,UAAU,MAAM,GAAG,GAAG,CAAC,sCAC7D;AAED,SAAQ,IAAI;EACV,QAAQ,IAAI,UAAU;EACtB;EACA,SAAS,eAAe,IAAI,QAAQ;EACpC,MAAM;EACN,UAAU;GAAE,QAAQ;GAAK,SAAS;GAAM;EACzC,CAAC;CAEF,MAAM,OAAmC,EACvC,WAAW,EACT,QAAQ,WACT,EACF;AACD,KAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,KAAI,IAAI,KAAK,UAAU,KAAK,CAAC"}
1
+ {"version":3,"file":"gemini-embeddings.js","names":[],"sources":["../src/gemini-embeddings.ts"],"sourcesContent":["/**\n * Google Gemini embedContent API support.\n *\n * Handles POST /v1beta/models/{model}:embedContent requests. Translates\n * incoming Gemini embedding requests into the ChatCompletionRequest format\n * used by the fixture router (with _endpointType: \"embedding\"), and converts\n * fixture responses back into the Gemini embedContent response format.\n *\n * Falls back to generating a deterministic embedding from the input text hash\n * when no fixture matches — same strategy as the OpenAI embeddings handler.\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 generateDeterministicEmbedding,\n MAX_EMBEDDING_DIMENSIONS,\n flattenHeaders,\n isJsonObject,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n getContext,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { matchFixtureDiagnostic } 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// ─── Gemini embedContent request types ────────────────────────────────────\n\ninterface GeminiEmbedContentPart {\n text?: string;\n}\n\ninterface GeminiEmbedContentRequest {\n content?: { parts?: GeminiEmbedContentPart[] };\n model?: string;\n taskType?: string;\n title?: string;\n outputDimensionality?: number;\n [key: string]: unknown;\n}\n\n// ─── Gemini embedContent response type ────────────────────────────────────\n\ninterface GeminiEmbedContentResponse {\n embedding: {\n values: number[];\n };\n}\n\n// ─── Default embedding dimensions for Gemini ──────────────────────────────\n\nconst DEFAULT_GEMINI_EMBEDDING_DIMENSIONS = 768;\n\n// ─── Request handler ───────────────────────────────────────────────────────\n\nexport async function handleGeminiEmbedContent(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n model: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n providerKey: RecordProviderKey = \"gemini\",\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let embedReq: GeminiEmbedContentRequest;\n try {\n embedReq = JSON.parse(raw) as GeminiEmbedContentRequest;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? `/v1beta/models/${model}:embedContent`,\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 code: 400,\n status: \"INVALID_ARGUMENT\",\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 `embedReq.content` throws a TypeError that\n // surfaces as a 500 instead of a 400.\n if (!isJsonObject(embedReq)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? `/v1beta/models/${model}:embedContent`,\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 code: 400,\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n\n // Extract text from content.parts\n const parts = embedReq.content?.parts ?? [];\n // Preserve nullish defaults and ignored non-null entries; guard only the\n // shapes that cannot be consumed by the text extraction below.\n const partsError = !Array.isArray(parts)\n ? \"content.parts must be an array\"\n : parts.some((part) => part === null)\n ? \"content.parts must not contain null entries\"\n : undefined;\n if (partsError) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? `/v1beta/models/${model}:embedContent`,\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: { message: partsError, code: 400, status: \"INVALID_ARGUMENT\" },\n }),\n );\n return;\n }\n const inputText = parts\n .filter((p) => p.text !== undefined)\n .map((p) => p.text!)\n .join(\" \");\n\n // Build a synthetic ChatCompletionRequest for the fixture router.\n // Uses _endpointType: \"embedding\" and embeddingInput to share fixtures\n // with OpenAI embeddings.\n const syntheticReq: ChatCompletionRequest = {\n model: embedReq.model ?? model,\n messages: [],\n embeddingInput: inputText,\n _endpointType: \"embedding\",\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n const path = req.url ?? `/v1beta/models/${model}:embedContent`;\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,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\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,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n const geminiError = {\n error: {\n code: status,\n message: response.error.message,\n status: response.error.type ?? \"ERROR\",\n },\n };\n writeErrorResponse(res, status, JSON.stringify(geminiError), {\n retryAfter: response.retryAfter,\n });\n return;\n }\n\n // Embedding response — use the fixture's embedding values\n if (isEmbeddingResponse(response)) {\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n const body: GeminiEmbedContentResponse = {\n embedding: {\n values: [...response.embedding],\n },\n };\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,\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 code: 500,\n status: \"INTERNAL\",\n },\n }),\n );\n return;\n }\n\n // No fixture match — check strict mode first, then try proxy\n const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n if (effectiveStrict) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n logger.error(strictNoMatchLogLine(req.method ?? \"POST\", path, skippedBySequenceOrTurn));\n journal.add({\n method: req.method ?? \"POST\",\n path,\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 code: 503,\n status: \"UNAVAILABLE\",\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 path,\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,\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 embedding from input text\n const dimensions = embedReq.outputDimensionality ?? DEFAULT_GEMINI_EMBEDDING_DIMENSIONS;\n // Match the embedding loop's numeric coercion, including array dimensions.\n // Preserve supported coercion and zero within the local serialization budget.\n // Guard only the fallback allocation, not unused dimensions on earlier branches.\n // Replay, strict, chaos, and proxy paths above do not consume this field.\n const exceedsFallbackBudget = dimensions > MAX_EMBEDDING_DIMENSIONS;\n if (\n exceedsFallbackBudget ||\n (typeof dimensions === \"number\" && (!Number.isInteger(dimensions) || dimensions < 0)) ||\n (typeof dimensions === \"string\" && Number.isNaN(Number(dimensions)))\n ) {\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: exceedsFallbackBudget\n ? `outputDimensionality exceeds the fallback serialization budget of ${MAX_EMBEDDING_DIMENSIONS}`\n : \"outputDimensionality cannot produce a numeric embedding\",\n code: 400,\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n const embedding = generateDeterministicEmbedding(inputText, dimensions);\n\n logger.warn(\n `No embedding fixture matched for \"${inputText.slice(0, 80)}\" — returning deterministic fallback`,\n );\n\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture: null },\n });\n\n const body: GeminiEmbedContentResponse = {\n embedding: {\n values: embedding,\n },\n };\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n}\n"],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  const require_helpers = require('./helpers.cjs');
2
2
  const require_sse_writer = require('./sse-writer.cjs');
3
3
  const require_chaos = require('./chaos.cjs');
4
+ const require_misbehavior = require('./misbehavior.cjs');
4
5
  const require_router = require('./router.cjs');
5
6
  const require_recorder = require('./recorder.cjs');
6
7
  const require_interruption = require('./interruption.cjs');
@@ -22,6 +23,29 @@ const STEP_TYPES = new Set([
22
23
  "model_output",
23
24
  ...STEP_RESULT_TYPES
24
25
  ]);
26
+ function validateInteractionsInput(input) {
27
+ if (typeof input === "number") return "input must not be a number";
28
+ if (!Array.isArray(input)) return null;
29
+ const first = input[0];
30
+ if (require_helpers.isJsonObject(first) && "role" in first) for (const [index, turn] of input.entries()) {
31
+ if (!require_helpers.isJsonObject(turn)) continue;
32
+ const blocks = turn.content ?? turn.parts;
33
+ if (typeof blocks === "string" && blocks.length > 0) return `input[${index}].${turn.content != null ? "content" : "parts"} must be an array`;
34
+ }
35
+ else if (!(require_helpers.isJsonObject(first) && typeof first.type === "string" && STEP_TYPES.has(first.type))) {
36
+ const nullIndex = input.findIndex((part) => part === null);
37
+ if (nullIndex !== -1) return `input[${nullIndex}] must be an object`;
38
+ }
39
+ return null;
40
+ }
41
+ function validateInteractionsTools(tools) {
42
+ if (typeof tools === "string" && tools.length > 0) return "tools must be an array";
43
+ if (Array.isArray(tools)) {
44
+ const nullIndex = tools.findIndex((tool) => tool === null);
45
+ if (nullIndex !== -1) return `tools[${nullIndex}] must be an object`;
46
+ }
47
+ return null;
48
+ }
25
49
  function geminiInteractionsToCompletionRequest(req) {
26
50
  const messages = [];
27
51
  const model = req.model ?? "gemini-2.5-flash";
@@ -192,18 +216,12 @@ function buildInteractionsTextResponse(content, model, interactionId, overrides)
192
216
  };
193
217
  }
194
218
  function buildFunctionCallStep(tc, logger) {
195
- let argsObj;
196
- try {
197
- argsObj = JSON.parse(tc.arguments || "{}");
198
- } catch {
199
- logger.warn(`Malformed JSON in fixture tool call arguments for "${tc.name}": ${tc.arguments}`);
200
- argsObj = {};
201
- }
219
+ const args = require_helpers.servedToolArgs(tc, "object", logger);
202
220
  return {
203
221
  type: "function_call",
204
222
  id: tc.id || require_helpers.generateToolCallId(),
205
223
  name: tc.name,
206
- arguments: argsObj
224
+ arguments: args.value
207
225
  };
208
226
  }
209
227
  function buildInteractionsToolCallResponse(toolCalls, model, interactionId, logger, overrides) {
@@ -337,13 +355,7 @@ function buildInteractionsToolCallSSEEvents(toolCalls, interactionId, logger, ov
337
355
  });
338
356
  for (let idx = 0; idx < toolCalls.length; idx++) {
339
357
  const tc = toolCalls[idx];
340
- let argsObj;
341
- try {
342
- argsObj = JSON.parse(tc.arguments || "{}");
343
- } catch {
344
- logger.warn(`Malformed JSON in fixture tool call arguments for "${tc.name}": ${tc.arguments}`);
345
- argsObj = {};
346
- }
358
+ const args = require_helpers.servedToolArgs(tc, "string", logger);
347
359
  events.push({
348
360
  event_type: "step.start",
349
361
  index: idx,
@@ -360,7 +372,7 @@ function buildInteractionsToolCallSSEEvents(toolCalls, interactionId, logger, ov
360
372
  index: idx,
361
373
  delta: {
362
374
  type: "arguments_delta",
363
- arguments: JSON.stringify(argsObj)
375
+ arguments: args.text
364
376
  },
365
377
  event_id: nextEventId()
366
378
  });
@@ -416,13 +428,7 @@ function pushTextStepEvents(events, index, content, chunkSize) {
416
428
  });
417
429
  }
418
430
  function pushFunctionCallStepEvents(events, index, tc, logger) {
419
- let argsObj;
420
- try {
421
- argsObj = JSON.parse(tc.arguments || "{}");
422
- } catch {
423
- logger.warn(`Malformed JSON in fixture tool call arguments for "${tc.name}": ${tc.arguments}`);
424
- argsObj = {};
425
- }
431
+ const args = require_helpers.servedToolArgs(tc, "string", logger);
426
432
  events.push({
427
433
  event_type: "step.start",
428
434
  index,
@@ -439,7 +445,7 @@ function pushFunctionCallStepEvents(events, index, tc, logger) {
439
445
  index,
440
446
  delta: {
441
447
  type: "arguments_delta",
442
- arguments: JSON.stringify(argsObj)
448
+ arguments: args.text
443
449
  },
444
450
  event_id: nextEventId()
445
451
  });
@@ -516,6 +522,35 @@ async function writeGeminiInteractionsSSEStream(res, events, optionsOrLatency) {
516
522
  if (!res.writableEnded) res.end();
517
523
  return true;
518
524
  }
525
+ function prepareInteractionsMisbehavior(plan, streaming) {
526
+ if (!streaming && plan.summary.fault === "stop-length-mid-tool") {
527
+ const response = plan.response;
528
+ const combined = require_helpers.isCombinedFixtureResponse(response);
529
+ const outcome = combined && response.blocks?.length ? require_helpers.resolveFixtureBlockOutcome(response.blocks) : void 0;
530
+ const toolCalls = (outcome?.toolCalls ?? (combined || require_helpers.isToolCallResponse(response) ? response.toolCalls ?? [] : [])).filter((_, index) => index !== plan.target?.index);
531
+ let callIndex = 0;
532
+ const blocks = outcome?.ordered.filter((block) => block.type === "text" || callIndex++ !== plan.target?.index);
533
+ plan = {
534
+ ...plan,
535
+ response: {
536
+ ...response,
537
+ toolCalls,
538
+ ...blocks ? { blocks } : {}
539
+ }
540
+ };
541
+ }
542
+ return require_helpers.prepareOpenAIChatMisbehavior(plan);
543
+ }
544
+ function finalizeInteractionsMisbehavior(events, plan) {
545
+ if (plan.kind !== "applied" || plan.summary.fault !== "stop-length-mid-tool") return events;
546
+ return events.map((event) => event.event_type === "interaction.completed" && require_helpers.isJsonObject(event.interaction) ? {
547
+ ...event,
548
+ interaction: {
549
+ ...event.interaction,
550
+ status: "incomplete"
551
+ }
552
+ } : event);
553
+ }
519
554
  async function handleGeminiInteractions(req, res, raw, fixtures, journal, defaults, setCorsHeaders) {
520
555
  const { logger } = defaults;
521
556
  setCorsHeaders(res);
@@ -552,6 +587,21 @@ async function handleGeminiInteractions(req, res, raw, fixtures, journal, defaul
552
587
  require_sse_writer.writeErrorResponse(res, 400, JSON.stringify(buildInteractionsErrorResponse("Request body must be a JSON object", "INVALID_ARGUMENT")));
553
588
  return;
554
589
  }
590
+ const validationError = validateInteractionsInput(interactionsReq.input) ?? validateInteractionsTools(interactionsReq.tools);
591
+ if (validationError) {
592
+ journal.add({
593
+ method: req.method ?? "POST",
594
+ path: urlPath,
595
+ headers: require_helpers.flattenHeaders(req.headers),
596
+ body: null,
597
+ response: {
598
+ status: 400,
599
+ fixture: null
600
+ }
601
+ });
602
+ require_sse_writer.writeErrorResponse(res, 400, JSON.stringify(buildInteractionsErrorResponse(validationError, "INVALID_ARGUMENT")));
603
+ return;
604
+ }
555
605
  const completionReq = geminiInteractionsToCompletionRequest(interactionsReq);
556
606
  completionReq._endpointType = "chat";
557
607
  completionReq._context = require_helpers.getContext(req);
@@ -617,29 +667,172 @@ async function handleGeminiInteractions(req, res, raw, fixtures, journal, defaul
617
667
  require_sse_writer.writeErrorResponse(res, 404, JSON.stringify(buildInteractionsErrorResponse("No fixture matched", "NOT_FOUND")));
618
668
  return;
619
669
  }
620
- const response = await require_helpers.resolveResponse(fixture, completionReq);
670
+ let response = await require_helpers.resolveResponse(fixture, completionReq);
671
+ let misbehavior = require_misbehavior.planMisbehavior({
672
+ wire: "gemini-interactions",
673
+ fixture,
674
+ response,
675
+ request: completionReq,
676
+ stream: streaming,
677
+ emitsToolCallIds: true,
678
+ defaults,
679
+ rawHeaders: req.headers,
680
+ url: req.url
681
+ });
682
+ if (misbehavior.kind === "error") {
683
+ require_misbehavior.recordMisbehaviorOutcome({
684
+ entry: journal.add({
685
+ method: req.method ?? "POST",
686
+ path: urlPath,
687
+ headers: require_helpers.flattenHeaders(req.headers),
688
+ body: completionReq,
689
+ response: {
690
+ status: misbehavior.status,
691
+ fixture
692
+ }
693
+ }),
694
+ summary: misbehavior.summary,
695
+ defaults,
696
+ testId
697
+ });
698
+ if (!misbehavior.summary?.evaluations.some((evaluation) => evaluation.outcome === "error")) logger.error(`${req.method ?? "POST"} ${urlPath}: ${misbehavior.message}`);
699
+ require_sse_writer.writeErrorResponse(res, misbehavior.status, JSON.stringify(buildInteractionsErrorResponse(misbehavior.message, misbehavior.code)));
700
+ return;
701
+ }
702
+ if (misbehavior.kind === "applied") {
703
+ misbehavior = prepareInteractionsMisbehavior(misbehavior, streaming);
704
+ response = misbehavior.response;
705
+ }
706
+ const appliedUsage = misbehavior.kind === "applied" ? require_helpers.resolveOpenAIChatMisbehaviorUsage(misbehavior, completionReq) : void 0;
621
707
  const latency = fixture.latency ?? defaults.latency;
622
708
  const chunkSize = Math.max(1, fixture.chunkSize ?? defaults.chunkSize);
623
709
  const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;
624
710
  if (require_helpers.isErrorResponse(response)) {
625
711
  const status = response.status ?? 500;
626
- journal.add({
712
+ require_misbehavior.recordMisbehaviorOutcome({
713
+ entry: journal.add({
714
+ method: req.method ?? "POST",
715
+ path: urlPath,
716
+ headers: require_helpers.flattenHeaders(req.headers),
717
+ body: completionReq,
718
+ response: {
719
+ status,
720
+ fixture
721
+ }
722
+ }),
723
+ summary: misbehavior.summary,
724
+ defaults,
725
+ testId
726
+ });
727
+ require_sse_writer.writeErrorResponse(res, status, JSON.stringify(buildInteractionsErrorResponse(response.error.message, response.error.type ?? "ERROR")), { retryAfter: response.retryAfter });
728
+ return;
729
+ }
730
+ if (misbehavior.kind === "applied" && misbehavior.summary.fault === "reasoning-only") {
731
+ const journalEntry = journal.add({
627
732
  method: req.method ?? "POST",
628
733
  path: urlPath,
629
734
  headers: require_helpers.flattenHeaders(req.headers),
630
735
  body: completionReq,
631
736
  response: {
632
- status,
737
+ status: 200,
633
738
  fixture
634
739
  }
635
740
  });
636
- require_sse_writer.writeErrorResponse(res, status, JSON.stringify(buildInteractionsErrorResponse(response.error.message, response.error.type ?? "ERROR")), { retryAfter: response.retryAfter });
741
+ require_misbehavior.recordMisbehaviorOutcome({
742
+ entry: journalEntry,
743
+ summary: misbehavior.summary,
744
+ defaults,
745
+ testId
746
+ });
747
+ const interactionId = nextInteractionId();
748
+ const usage = {
749
+ ...interactionsUsage({ usage: appliedUsage }),
750
+ total_output_tokens: 0,
751
+ total_thought_tokens: appliedUsage?.completion_tokens ?? 0
752
+ };
753
+ if (!streaming) {
754
+ const overrides = require_helpers.isTextResponse(response) || require_helpers.isToolCallResponse(response) || require_helpers.isCombinedFixtureResponse(response) ? require_helpers.extractOverrides(response) : void 0;
755
+ res.writeHead(200, { "Content-Type": "application/json" });
756
+ res.end(JSON.stringify({
757
+ id: interactionId,
758
+ model: overrides?.model ?? model,
759
+ status: "incomplete",
760
+ steps: [{
761
+ type: "thought",
762
+ signature: "",
763
+ summary: [{
764
+ type: "text",
765
+ text: misbehavior.reasoning ?? ""
766
+ }]
767
+ }],
768
+ usage
769
+ }));
770
+ return;
771
+ }
772
+ const events = [
773
+ {
774
+ event_type: "interaction.created",
775
+ interaction: {
776
+ id: interactionId,
777
+ status: "in_progress"
778
+ },
779
+ event_id: nextEventId()
780
+ },
781
+ {
782
+ event_type: "step.start",
783
+ index: 0,
784
+ step: { type: "thought" },
785
+ event_id: nextEventId()
786
+ },
787
+ {
788
+ event_type: "step.delta",
789
+ index: 0,
790
+ delta: {
791
+ type: "thought_summary",
792
+ content: {
793
+ type: "text",
794
+ text: misbehavior.reasoning ?? ""
795
+ }
796
+ },
797
+ event_id: nextEventId()
798
+ },
799
+ {
800
+ event_type: "step.stop",
801
+ index: 0,
802
+ event_id: nextEventId()
803
+ },
804
+ {
805
+ event_type: "interaction.completed",
806
+ interaction: {
807
+ id: interactionId,
808
+ status: "incomplete",
809
+ usage
810
+ },
811
+ event_id: nextEventId()
812
+ }
813
+ ];
814
+ const interruption = require_interruption.createInterruptionSignal(fixture);
815
+ if (!await writeGeminiInteractionsSSEStream(res, events, {
816
+ latency,
817
+ streamingProfile: fixture.streamingProfile,
818
+ recordedTimings: fixture.recordedTimings,
819
+ replaySpeed,
820
+ signal: interruption?.signal,
821
+ onChunkSent: interruption?.tick
822
+ })) {
823
+ if (!res.writableEnded) res.destroy();
824
+ journalEntry.response.interrupted = true;
825
+ journalEntry.response.interruptReason = interruption?.reason();
826
+ }
827
+ interruption?.cleanup();
637
828
  return;
638
829
  }
639
- const interactionId = nextInteractionId();
640
- if (require_helpers.isContentWithToolCallsResponse(response)) {
830
+ if (require_helpers.isCombinedFixtureResponse(response)) {
641
831
  if (response.webSearches?.length) logger.warn("webSearches in fixture response are not supported for Gemini Interactions API — ignoring");
642
- const overrides = require_helpers.extractOverrides(response);
832
+ const overrides = {
833
+ ...require_helpers.extractOverrides(response),
834
+ ...appliedUsage ? { usage: appliedUsage } : {}
835
+ };
643
836
  const journalEntry = journal.add({
644
837
  method: req.method ?? "POST",
645
838
  path: urlPath,
@@ -650,14 +843,25 @@ async function handleGeminiInteractions(req, res, raw, fixtures, journal, defaul
650
843
  fixture
651
844
  }
652
845
  });
846
+ require_misbehavior.recordMisbehaviorOutcome({
847
+ entry: journalEntry,
848
+ summary: misbehavior.summary,
849
+ defaults,
850
+ testId
851
+ });
852
+ const functionToolCalls = require_helpers.requireEmittedFunctionToolCalls(response, "Gemini Interactions");
853
+ const interactionId = nextInteractionId();
653
854
  if (!streaming) {
654
- const body = buildInteractionsContentWithToolCallsResponse(response.content ?? "", response.toolCalls ?? [], model, interactionId, logger, overrides, response.blocks);
855
+ const body = buildInteractionsContentWithToolCallsResponse(response.content ?? "", functionToolCalls, model, interactionId, logger, overrides, response.blocks);
655
856
  res.writeHead(200, { "Content-Type": "application/json" });
656
- res.end(JSON.stringify(body));
857
+ res.end(JSON.stringify(misbehavior.kind === "applied" && misbehavior.summary.fault === "stop-length-mid-tool" ? {
858
+ ...body,
859
+ status: "incomplete"
860
+ } : body));
657
861
  } else {
658
- const events = buildInteractionsContentWithToolCallsSSEEvents(response.content ?? "", response.toolCalls ?? [], interactionId, chunkSize, logger, overrides, response.blocks);
862
+ const events = buildInteractionsContentWithToolCallsSSEEvents(response.content ?? "", functionToolCalls, interactionId, chunkSize, logger, overrides, response.blocks);
659
863
  const interruption = require_interruption.createInterruptionSignal(fixture);
660
- if (!await writeGeminiInteractionsSSEStream(res, events, {
864
+ if (!await writeGeminiInteractionsSSEStream(res, finalizeInteractionsMisbehavior(events, misbehavior), {
661
865
  latency,
662
866
  streamingProfile: fixture.streamingProfile,
663
867
  recordedTimings: fixture.recordedTimings,
@@ -675,7 +879,10 @@ async function handleGeminiInteractions(req, res, raw, fixtures, journal, defaul
675
879
  }
676
880
  if (require_helpers.isTextResponse(response)) {
677
881
  if (response.webSearches?.length) logger.warn("webSearches in fixture response are not supported for Gemini Interactions API — ignoring");
678
- const overrides = require_helpers.extractOverrides(response);
882
+ const overrides = {
883
+ ...require_helpers.extractOverrides(response),
884
+ ...appliedUsage ? { usage: appliedUsage } : {}
885
+ };
679
886
  const journalEntry = journal.add({
680
887
  method: req.method ?? "POST",
681
888
  path: urlPath,
@@ -686,14 +893,24 @@ async function handleGeminiInteractions(req, res, raw, fixtures, journal, defaul
686
893
  fixture
687
894
  }
688
895
  });
896
+ require_misbehavior.recordMisbehaviorOutcome({
897
+ entry: journalEntry,
898
+ summary: misbehavior.summary,
899
+ defaults,
900
+ testId
901
+ });
902
+ const interactionId = nextInteractionId();
689
903
  if (!streaming) {
690
904
  const body = buildInteractionsTextResponse(response.content, model, interactionId, overrides);
691
905
  res.writeHead(200, { "Content-Type": "application/json" });
692
- res.end(JSON.stringify(body));
906
+ res.end(JSON.stringify(misbehavior.kind === "applied" && misbehavior.summary.fault === "stop-length-mid-tool" ? {
907
+ ...body,
908
+ status: "incomplete"
909
+ } : body));
693
910
  } else {
694
911
  const events = buildInteractionsTextSSEEvents(response.content, interactionId, chunkSize, overrides);
695
912
  const interruption = require_interruption.createInterruptionSignal(fixture);
696
- if (!await writeGeminiInteractionsSSEStream(res, events, {
913
+ if (!await writeGeminiInteractionsSSEStream(res, finalizeInteractionsMisbehavior(events, misbehavior), {
697
914
  latency,
698
915
  streamingProfile: fixture.streamingProfile,
699
916
  recordedTimings: fixture.recordedTimings,
@@ -711,7 +928,10 @@ async function handleGeminiInteractions(req, res, raw, fixtures, journal, defaul
711
928
  }
712
929
  if (require_helpers.isToolCallResponse(response)) {
713
930
  if (response.webSearches?.length) logger.warn("webSearches in fixture response are not supported for Gemini Interactions API — ignoring");
714
- const overrides = require_helpers.extractOverrides(response);
931
+ const overrides = {
932
+ ...require_helpers.extractOverrides(response),
933
+ ...appliedUsage ? { usage: appliedUsage } : {}
934
+ };
715
935
  const journalEntry = journal.add({
716
936
  method: req.method ?? "POST",
717
937
  path: urlPath,
@@ -722,14 +942,25 @@ async function handleGeminiInteractions(req, res, raw, fixtures, journal, defaul
722
942
  fixture
723
943
  }
724
944
  });
945
+ require_misbehavior.recordMisbehaviorOutcome({
946
+ entry: journalEntry,
947
+ summary: misbehavior.summary,
948
+ defaults,
949
+ testId
950
+ });
951
+ const functionToolCalls = require_helpers.requireEmittedFunctionToolCalls(response, "Gemini Interactions");
952
+ const interactionId = nextInteractionId();
725
953
  if (!streaming) {
726
- const body = buildInteractionsToolCallResponse(response.toolCalls, model, interactionId, logger, overrides);
954
+ const body = buildInteractionsToolCallResponse(functionToolCalls, model, interactionId, logger, overrides);
727
955
  res.writeHead(200, { "Content-Type": "application/json" });
728
- res.end(JSON.stringify(body));
956
+ res.end(JSON.stringify(misbehavior.kind === "applied" && misbehavior.summary.fault === "stop-length-mid-tool" ? {
957
+ ...body,
958
+ status: "incomplete"
959
+ } : body));
729
960
  } else {
730
- const events = buildInteractionsToolCallSSEEvents(response.toolCalls, interactionId, logger, overrides);
961
+ const events = buildInteractionsToolCallSSEEvents(functionToolCalls, interactionId, logger, overrides);
731
962
  const interruption = require_interruption.createInterruptionSignal(fixture);
732
- if (!await writeGeminiInteractionsSSEStream(res, events, {
963
+ if (!await writeGeminiInteractionsSSEStream(res, finalizeInteractionsMisbehavior(events, misbehavior), {
733
964
  latency,
734
965
  streamingProfile: fixture.streamingProfile,
735
966
  recordedTimings: fixture.recordedTimings,
@@ -745,15 +976,20 @@ async function handleGeminiInteractions(req, res, raw, fixtures, journal, defaul
745
976
  }
746
977
  return;
747
978
  }
748
- journal.add({
749
- method: req.method ?? "POST",
750
- path: urlPath,
751
- headers: require_helpers.flattenHeaders(req.headers),
752
- body: completionReq,
753
- response: {
754
- status: 500,
755
- fixture
756
- }
979
+ require_misbehavior.recordMisbehaviorOutcome({
980
+ entry: journal.add({
981
+ method: req.method ?? "POST",
982
+ path: urlPath,
983
+ headers: require_helpers.flattenHeaders(req.headers),
984
+ body: completionReq,
985
+ response: {
986
+ status: 500,
987
+ fixture
988
+ }
989
+ }),
990
+ summary: misbehavior.summary,
991
+ defaults,
992
+ testId
757
993
  });
758
994
  require_sse_writer.writeErrorResponse(res, 500, JSON.stringify(buildInteractionsErrorResponse("Fixture response did not match any known type", "INTERNAL")));
759
995
  }