@copilotkit/aimock 1.44.0 → 1.45.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +42 -0
  4. package/README.md +6 -0
  5. package/dist/agui-types.d.cts.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 +312 -39
  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 +313 -40
  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 +1 -0
  25. package/dist/chaos.js +1 -1
  26. package/dist/cli.cjs +152 -5
  27. package/dist/cli.cjs.map +1 -1
  28. package/dist/cli.js +155 -8
  29. package/dist/cli.js.map +1 -1
  30. package/dist/cohere.cjs +132 -71
  31. package/dist/cohere.cjs.map +1 -1
  32. package/dist/cohere.d.cts.map +1 -1
  33. package/dist/cohere.d.ts.map +1 -1
  34. package/dist/cohere.js +133 -72
  35. package/dist/cohere.js.map +1 -1
  36. package/dist/config-loader.cjs +62 -4
  37. package/dist/config-loader.cjs.map +1 -1
  38. package/dist/config-loader.d.cts +37 -4
  39. package/dist/config-loader.d.cts.map +1 -1
  40. package/dist/config-loader.d.ts +37 -4
  41. package/dist/config-loader.d.ts.map +1 -1
  42. package/dist/config-loader.js +62 -4
  43. package/dist/config-loader.js.map +1 -1
  44. package/dist/fakes-plugin.cjs +131 -0
  45. package/dist/fakes-plugin.cjs.map +1 -0
  46. package/dist/fakes-plugin.d.cts +23 -0
  47. package/dist/fakes-plugin.d.cts.map +1 -0
  48. package/dist/fakes-plugin.d.ts +23 -0
  49. package/dist/fakes-plugin.d.ts.map +1 -0
  50. package/dist/fakes-plugin.js +127 -0
  51. package/dist/fakes-plugin.js.map +1 -0
  52. package/dist/fixture-loader-services.cjs +1 -1
  53. package/dist/fixture-loader-services.cjs.map +1 -1
  54. package/dist/fixture-loader-services.js +1 -1
  55. package/dist/fixture-loader-services.js.map +1 -1
  56. package/dist/fixture-loader.cjs +389 -27
  57. package/dist/fixture-loader.cjs.map +1 -1
  58. package/dist/fixture-loader.d.cts +47 -3
  59. package/dist/fixture-loader.d.cts.map +1 -1
  60. package/dist/fixture-loader.d.ts +47 -3
  61. package/dist/fixture-loader.d.ts.map +1 -1
  62. package/dist/fixture-loader.js +384 -29
  63. package/dist/fixture-loader.js.map +1 -1
  64. package/dist/gemini-interactions.cjs +250 -52
  65. package/dist/gemini-interactions.cjs.map +1 -1
  66. package/dist/gemini-interactions.d.cts.map +1 -1
  67. package/dist/gemini-interactions.d.ts.map +1 -1
  68. package/dist/gemini-interactions.js +251 -53
  69. package/dist/gemini-interactions.js.map +1 -1
  70. package/dist/gemini.cjs +226 -37
  71. package/dist/gemini.cjs.map +1 -1
  72. package/dist/gemini.d.cts.map +1 -1
  73. package/dist/gemini.d.ts.map +1 -1
  74. package/dist/gemini.js +227 -38
  75. package/dist/gemini.js.map +1 -1
  76. package/dist/helpers.cjs +881 -7
  77. package/dist/helpers.cjs.map +1 -1
  78. package/dist/helpers.d.cts +85 -4
  79. package/dist/helpers.d.cts.map +1 -1
  80. package/dist/helpers.d.ts +85 -4
  81. package/dist/helpers.d.ts.map +1 -1
  82. package/dist/helpers.js +839 -8
  83. package/dist/helpers.js.map +1 -1
  84. package/dist/index.cjs +9 -0
  85. package/dist/index.d.cts +7 -5
  86. package/dist/index.d.ts +7 -5
  87. package/dist/index.js +4 -3
  88. package/dist/jest.cjs +31 -7
  89. package/dist/jest.cjs.map +1 -1
  90. package/dist/jest.d.cts +43 -3
  91. package/dist/jest.d.cts.map +1 -1
  92. package/dist/jest.d.ts +43 -3
  93. package/dist/jest.d.ts.map +1 -1
  94. package/dist/jest.js +33 -9
  95. package/dist/jest.js.map +1 -1
  96. package/dist/journal.cjs +135 -13
  97. package/dist/journal.cjs.map +1 -1
  98. package/dist/journal.d.cts +38 -4
  99. package/dist/journal.d.cts.map +1 -1
  100. package/dist/journal.d.ts +38 -4
  101. package/dist/journal.d.ts.map +1 -1
  102. package/dist/journal.js +135 -13
  103. package/dist/journal.js.map +1 -1
  104. package/dist/live-sanitize.cjs +4 -32
  105. package/dist/live-sanitize.cjs.map +1 -1
  106. package/dist/live-sanitize.js +4 -32
  107. package/dist/live-sanitize.js.map +1 -1
  108. package/dist/llmock.cjs +123 -30
  109. package/dist/llmock.cjs.map +1 -1
  110. package/dist/llmock.d.cts +16 -1
  111. package/dist/llmock.d.cts.map +1 -1
  112. package/dist/llmock.d.ts +16 -1
  113. package/dist/llmock.d.ts.map +1 -1
  114. package/dist/llmock.js +125 -32
  115. package/dist/llmock.js.map +1 -1
  116. package/dist/mcp-fakes-mount.cjs +1 -0
  117. package/dist/mcp-fakes-mount.cjs.map +1 -1
  118. package/dist/mcp-fakes-mount.js +1 -0
  119. package/dist/mcp-fakes-mount.js.map +1 -1
  120. package/dist/mcp-fakes-report.cjs +111 -0
  121. package/dist/mcp-fakes-report.cjs.map +1 -0
  122. package/dist/mcp-fakes-report.d.cts +40 -0
  123. package/dist/mcp-fakes-report.d.cts.map +1 -0
  124. package/dist/mcp-fakes-report.d.ts +40 -0
  125. package/dist/mcp-fakes-report.d.ts.map +1 -0
  126. package/dist/mcp-fakes-report.js +107 -0
  127. package/dist/mcp-fakes-report.js.map +1 -0
  128. package/dist/mcp-fakes.cjs +331 -8
  129. package/dist/mcp-fakes.cjs.map +1 -1
  130. package/dist/mcp-fakes.d.cts +406 -3
  131. package/dist/mcp-fakes.d.cts.map +1 -1
  132. package/dist/mcp-fakes.d.ts +406 -3
  133. package/dist/mcp-fakes.d.ts.map +1 -1
  134. package/dist/mcp-fakes.js +331 -8
  135. package/dist/mcp-fakes.js.map +1 -1
  136. package/dist/mcp-handler.cjs +216 -17
  137. package/dist/mcp-handler.cjs.map +1 -1
  138. package/dist/mcp-handler.d.cts +49 -0
  139. package/dist/mcp-handler.d.cts.map +1 -0
  140. package/dist/mcp-handler.d.ts +49 -0
  141. package/dist/mcp-handler.d.ts.map +1 -0
  142. package/dist/mcp-handler.js +214 -18
  143. package/dist/mcp-handler.js.map +1 -1
  144. package/dist/mcp-mock.cjs +112 -1
  145. package/dist/mcp-mock.cjs.map +1 -1
  146. package/dist/mcp-mock.d.cts +33 -2
  147. package/dist/mcp-mock.d.cts.map +1 -1
  148. package/dist/mcp-mock.d.ts +33 -2
  149. package/dist/mcp-mock.d.ts.map +1 -1
  150. package/dist/mcp-mock.js +112 -1
  151. package/dist/mcp-mock.js.map +1 -1
  152. package/dist/mcp-recorder.cjs +1224 -0
  153. package/dist/mcp-recorder.cjs.map +1 -0
  154. package/dist/mcp-recorder.d.cts +108 -0
  155. package/dist/mcp-recorder.d.cts.map +1 -0
  156. package/dist/mcp-recorder.d.ts +108 -0
  157. package/dist/mcp-recorder.d.ts.map +1 -0
  158. package/dist/mcp-recorder.js +1215 -0
  159. package/dist/mcp-recorder.js.map +1 -0
  160. package/dist/messages.cjs +362 -56
  161. package/dist/messages.cjs.map +1 -1
  162. package/dist/messages.d.cts.map +1 -1
  163. package/dist/messages.d.ts.map +1 -1
  164. package/dist/messages.js +363 -58
  165. package/dist/messages.js.map +1 -1
  166. package/dist/misbehavior.cjs +1523 -0
  167. package/dist/misbehavior.cjs.map +1 -0
  168. package/dist/misbehavior.d.cts +43 -0
  169. package/dist/misbehavior.d.cts.map +1 -0
  170. package/dist/misbehavior.d.ts +43 -0
  171. package/dist/misbehavior.d.ts.map +1 -0
  172. package/dist/misbehavior.js +1511 -0
  173. package/dist/misbehavior.js.map +1 -0
  174. package/dist/ollama.cjs +310 -112
  175. package/dist/ollama.cjs.map +1 -1
  176. package/dist/ollama.d.cts.map +1 -1
  177. package/dist/ollama.d.ts.map +1 -1
  178. package/dist/ollama.js +311 -113
  179. package/dist/ollama.js.map +1 -1
  180. package/dist/openrouter-video.cjs +30 -2
  181. package/dist/openrouter-video.cjs.map +1 -1
  182. package/dist/openrouter-video.d.cts.map +1 -1
  183. package/dist/openrouter-video.d.ts.map +1 -1
  184. package/dist/openrouter-video.js +30 -2
  185. package/dist/openrouter-video.js.map +1 -1
  186. package/dist/record-sanitize.cjs +231 -0
  187. package/dist/record-sanitize.cjs.map +1 -0
  188. package/dist/record-sanitize.js +224 -0
  189. package/dist/record-sanitize.js.map +1 -0
  190. package/dist/recorder.cjs +106 -5
  191. package/dist/recorder.cjs.map +1 -1
  192. package/dist/recorder.d.cts +2 -1
  193. package/dist/recorder.d.cts.map +1 -1
  194. package/dist/recorder.d.ts +2 -1
  195. package/dist/recorder.d.ts.map +1 -1
  196. package/dist/recorder.js +107 -7
  197. package/dist/recorder.js.map +1 -1
  198. package/dist/responses.cjs +667 -171
  199. package/dist/responses.cjs.map +1 -1
  200. package/dist/responses.d.cts +5 -13
  201. package/dist/responses.d.cts.map +1 -1
  202. package/dist/responses.d.ts +5 -13
  203. package/dist/responses.d.ts.map +1 -1
  204. package/dist/responses.js +665 -174
  205. package/dist/responses.js.map +1 -1
  206. package/dist/router.cjs +24 -2
  207. package/dist/router.cjs.map +1 -1
  208. package/dist/router.d.cts.map +1 -1
  209. package/dist/router.d.ts.map +1 -1
  210. package/dist/router.js +25 -4
  211. package/dist/router.js.map +1 -1
  212. package/dist/server.cjs +572 -76
  213. package/dist/server.cjs.map +1 -1
  214. package/dist/server.d.cts.map +1 -1
  215. package/dist/server.d.ts.map +1 -1
  216. package/dist/server.js +574 -78
  217. package/dist/server.js.map +1 -1
  218. package/dist/stream-collapse.cjs +236 -34
  219. package/dist/stream-collapse.cjs.map +1 -1
  220. package/dist/stream-collapse.d.cts +39 -4
  221. package/dist/stream-collapse.d.cts.map +1 -1
  222. package/dist/stream-collapse.d.ts +39 -4
  223. package/dist/stream-collapse.d.ts.map +1 -1
  224. package/dist/stream-collapse.js +235 -35
  225. package/dist/stream-collapse.js.map +1 -1
  226. package/dist/types.cjs.map +1 -1
  227. package/dist/types.d.cts +315 -4
  228. package/dist/types.d.cts.map +1 -1
  229. package/dist/types.d.ts +315 -4
  230. package/dist/types.d.ts.map +1 -1
  231. package/dist/types.js.map +1 -1
  232. package/dist/validate-cli.cjs +36 -4
  233. package/dist/validate-cli.cjs.map +1 -1
  234. package/dist/validate-cli.js +37 -5
  235. package/dist/validate-cli.js.map +1 -1
  236. package/dist/vector-types.d.ts.map +1 -1
  237. package/dist/version.cjs +45 -0
  238. package/dist/version.cjs.map +1 -0
  239. package/dist/version.js +44 -0
  240. package/dist/version.js.map +1 -0
  241. package/dist/vitest.cjs +66 -10
  242. package/dist/vitest.cjs.map +1 -1
  243. package/dist/vitest.d.cts +35 -4
  244. package/dist/vitest.d.cts.map +1 -1
  245. package/dist/vitest.d.ts +35 -4
  246. package/dist/vitest.d.ts.map +1 -1
  247. package/dist/vitest.js +67 -11
  248. package/dist/vitest.js.map +1 -1
  249. package/dist/watcher.cjs +6 -2
  250. package/dist/watcher.cjs.map +1 -1
  251. package/dist/watcher.js +6 -2
  252. package/dist/watcher.js.map +1 -1
  253. package/dist/ws-gemini-live.cjs +210 -101
  254. package/dist/ws-gemini-live.cjs.map +1 -1
  255. package/dist/ws-gemini-live.d.cts +9 -3
  256. package/dist/ws-gemini-live.d.cts.map +1 -1
  257. package/dist/ws-gemini-live.d.ts +9 -3
  258. package/dist/ws-gemini-live.d.ts.map +1 -1
  259. package/dist/ws-gemini-live.js +211 -102
  260. package/dist/ws-gemini-live.js.map +1 -1
  261. package/dist/ws-realtime.cjs +175 -62
  262. package/dist/ws-realtime.cjs.map +1 -1
  263. package/dist/ws-realtime.d.cts +8 -3
  264. package/dist/ws-realtime.d.cts.map +1 -1
  265. package/dist/ws-realtime.d.ts +8 -3
  266. package/dist/ws-realtime.d.ts.map +1 -1
  267. package/dist/ws-realtime.js +176 -63
  268. package/dist/ws-realtime.js.map +1 -1
  269. package/dist/ws-responses.cjs +109 -16
  270. package/dist/ws-responses.cjs.map +1 -1
  271. package/dist/ws-responses.d.cts +8 -12
  272. package/dist/ws-responses.d.cts.map +1 -1
  273. package/dist/ws-responses.d.ts +8 -12
  274. package/dist/ws-responses.d.ts.map +1 -1
  275. package/dist/ws-responses.js +111 -18
  276. package/dist/ws-responses.js.map +1 -1
  277. package/package.json +10 -2
  278. package/skills/write-fixtures/SKILL.md +179 -21
package/dist/gemini.js CHANGED
@@ -1,6 +1,7 @@
1
- import { extractOverrides, flattenHeaders, formatToMime, getContext, getTestId, isAudioResponse, isContentWithToolCallsResponse, isErrorResponse, isJsonObject, isTextResponse, isToolCallResponse, resolveFixtureBlockOutcome, resolveReasoningForModel, resolveResponse, resolveStrictMode, strictNoMatchLogLine, strictNoMatchMessage, strictOverrideField, validateToolsField } from "./helpers.js";
1
+ import { extractOverrides, flattenHeaders, formatToMime, getContext, getTestId, isAudioResponse, isCombinedFixtureResponse, isErrorResponse, isJsonObject, isTextResponse, isToolCallResponse, requireEmittedFunctionToolCalls, requireServedFunctionToolCalls, resolveFixtureBlockOutcome, resolveReasoningForModel, resolveResponse, resolveStrictMode, resolveUsage, servedToolArgs, strictNoMatchLogLine, strictNoMatchMessage, strictOverrideField, validateToolsField } from "./helpers.js";
2
2
  import { calculateDelay, delay, writeErrorResponse } from "./sse-writer.js";
3
3
  import { applyChaosAsync } from "./chaos.js";
4
+ import { planMisbehavior, recordMisbehaviorOutcome } from "./misbehavior.js";
4
5
  import { matchFixtureDiagnostic, recordMatchOptions } from "./router.js";
5
6
  import { proxyAndRecord } from "./recorder.js";
6
7
  import { createInterruptionSignal } from "./interruption.js";
@@ -115,7 +116,7 @@ function geminiToCompletionRequest(req, model, stream) {
115
116
  function: {
116
117
  name: d.name,
117
118
  description: d.description,
118
- parameters: d.parameters
119
+ parameters: d.parameters ?? d.parametersJsonSchema
119
120
  }
120
121
  }));
121
122
  }
@@ -199,16 +200,10 @@ function buildGeminiTextStreamChunks(content, chunkSize, reasoning, overrides) {
199
200
  return chunks;
200
201
  }
201
202
  function parseToolCallPart(tc, logger) {
202
- let argsObj;
203
- try {
204
- argsObj = JSON.parse(tc.arguments || "{}");
205
- } catch {
206
- logger.warn(`Malformed JSON in fixture tool call arguments for "${tc.name}": ${tc.arguments}`);
207
- argsObj = {};
208
- }
203
+ const args = servedToolArgs(tc, "object", logger);
209
204
  const functionCall = {
210
205
  name: tc.name,
211
- args: argsObj
206
+ args: args.value
212
207
  };
213
208
  if (tc.id) functionCall.id = tc.id;
214
209
  return { functionCall };
@@ -425,24 +420,24 @@ function resolveAudioInlineData(audio) {
425
420
  data: audio.audio.b64Json
426
421
  };
427
422
  }
428
- function buildGeminiAudioParts(audio, logger, effReasoning) {
423
+ function buildGeminiAudioParts(audio, toolCalls, logger, effReasoning) {
429
424
  const parts = [{ inlineData: resolveAudioInlineData(audio) }];
430
425
  if (effReasoning) parts.push({
431
426
  text: effReasoning,
432
427
  thought: true
433
428
  });
434
429
  if (audio.content) parts.push({ text: audio.content });
435
- if (audio.toolCalls?.length) parts.push(...audio.toolCalls.map((tc) => parseToolCallPart(tc, logger)));
430
+ if (toolCalls.length) parts.push(...toolCalls.map((tc) => parseToolCallPart(tc, logger)));
436
431
  return parts;
437
432
  }
438
- function buildGeminiAudioResponse(audio, logger, effReasoning) {
433
+ function buildGeminiAudioResponse(audio, toolCalls, logger, effReasoning) {
439
434
  return {
440
435
  candidates: [{
441
436
  content: {
442
437
  role: "model",
443
- parts: buildGeminiAudioParts(audio, logger, effReasoning)
438
+ parts: buildGeminiAudioParts(audio, toolCalls, logger, effReasoning)
444
439
  },
445
- finishReason: audio.toolCalls?.length ? "FUNCTION_CALL" : "STOP",
440
+ finishReason: toolCalls.length ? "FUNCTION_CALL" : "STOP",
446
441
  index: 0
447
442
  }],
448
443
  usageMetadata: {
@@ -452,14 +447,14 @@ function buildGeminiAudioResponse(audio, logger, effReasoning) {
452
447
  }
453
448
  };
454
449
  }
455
- function buildGeminiAudioStreamChunks(audio, logger, effReasoning) {
450
+ function buildGeminiAudioStreamChunks(audio, toolCalls, logger, effReasoning) {
456
451
  return [{
457
452
  candidates: [{
458
453
  content: {
459
454
  role: "model",
460
- parts: buildGeminiAudioParts(audio, logger, effReasoning)
455
+ parts: buildGeminiAudioParts(audio, toolCalls, logger, effReasoning)
461
456
  },
462
- finishReason: audio.toolCalls?.length ? "FUNCTION_CALL" : "STOP",
457
+ finishReason: toolCalls.length ? "FUNCTION_CALL" : "STOP",
463
458
  index: 0
464
459
  }],
465
460
  usageMetadata: {
@@ -494,8 +489,90 @@ async function writeGeminiSSEStream(res, chunks, optionsOrLatency) {
494
489
  if (!res.writableEnded) res.end();
495
490
  return true;
496
491
  }
492
+ /** Prepare native fault output once; the journal describes these exact parts. */
493
+ function prepareGeminiMisbehavior(plan, request, streaming, chunkSize, logger, strict) {
494
+ const response = plan.response;
495
+ const combined = isCombinedFixtureResponse(response);
496
+ if (!(combined || isToolCallResponse(response) || isTextResponse(response))) throw new Error("Applied Gemini misbehavior requires a chat response");
497
+ const outcome = combined && response.blocks?.length ? resolveFixtureBlockOutcome(response.blocks) : void 0;
498
+ const content = outcome?.content ?? ("content" in response ? response.content ?? "" : "");
499
+ const calls = outcome?.toolCalls ?? ("toolCalls" in response ? response.toolCalls ?? [] : []);
500
+ const overrides = extractOverrides(response);
501
+ delete overrides.usage;
502
+ const fault = plan.summary.fault;
503
+ const candidate = {
504
+ content: {
505
+ role: "model",
506
+ parts: []
507
+ },
508
+ index: 0
509
+ };
510
+ let chunks;
511
+ if (fault === "tool-args-invalid-json") {
512
+ const target = calls[plan.target?.index ?? 0];
513
+ if (!target) throw new Error("Gemini malformed-call plan requires its selected tool call");
514
+ chunks = [{ candidates: [{
515
+ index: 0,
516
+ finishReason: "MALFORMED_FUNCTION_CALL",
517
+ finishMessage: `Malformed function call: ${target.name}(${target.arguments})`
518
+ }] }];
519
+ } else if (fault === "tool-unknown-name" && !request.tools?.length) {
520
+ candidate.finishReason = "UNEXPECTED_TOOL_CALL";
521
+ chunks = [{ candidates: [candidate] }];
522
+ } else if (fault === "stop-length-mid-tool") {
523
+ candidate.content.parts = content ? [{ text: content }] : [];
524
+ candidate.finishReason = "MAX_TOKENS";
525
+ chunks = [{ candidates: [candidate] }];
526
+ } else if (fault === "empty-response") {
527
+ candidate.content.parts = [{ text: "" }];
528
+ candidate.finishReason = "STOP";
529
+ chunks = [{ candidates: [candidate] }];
530
+ } else if (fault === "content-filter") {
531
+ candidate.finishReason = "SAFETY";
532
+ candidate.safetyRatings = [{
533
+ category: "HARM_CATEGORY_DANGEROUS_CONTENT",
534
+ probability: "HIGH",
535
+ blocked: true
536
+ }];
537
+ chunks = [{ candidates: [candidate] }];
538
+ } else if (fault === "reasoning-only") {
539
+ candidate.content.parts = [{
540
+ text: plan.reasoning ?? "",
541
+ thought: true
542
+ }];
543
+ candidate.finishReason = "MAX_TOKENS";
544
+ chunks = [{ candidates: [candidate] }];
545
+ } else {
546
+ const reasoning = resolveReasoningForModel(response.reasoning, request.model, strict, logger);
547
+ if (fault === "tool-unknown-name") overrides.finishReason = "stop";
548
+ if (combined) chunks = streaming ? buildGeminiContentWithToolCallsStreamChunks(content, calls, chunkSize, logger, reasoning, overrides, response.blocks) : [buildGeminiContentWithToolCallsResponse(content, calls, logger, reasoning, overrides, response.blocks)];
549
+ else if (isToolCallResponse(response)) chunks = streaming ? buildGeminiToolCallStreamChunks(calls, chunkSize, logger, reasoning, overrides) : [buildGeminiToolCallResponse(calls, logger, reasoning, overrides)];
550
+ else chunks = streaming ? buildGeminiTextStreamChunks(content, chunkSize, reasoning, overrides) : [buildGeminiTextResponse(content, reasoning, overrides)];
551
+ }
552
+ const parts = chunks.flatMap((chunk) => chunk.candidates.flatMap((item) => item.content?.parts ?? []));
553
+ const servedToolCalls = parts.flatMap((part) => part.functionCall ? [{
554
+ name: part.functionCall.name,
555
+ arguments: JSON.stringify(part.functionCall.args),
556
+ ...part.functionCall.id ? { id: part.functionCall.id } : {}
557
+ }] : []);
558
+ const usage = resolveUsage(void 0, request.messages.map((message) => typeof message.content === "string" ? message.content : (message.content ?? []).map((part) => part.text ?? "").join("")).join(""), parts.map((part) => part.text ?? (part.functionCall ? part.functionCall.name + JSON.stringify(part.functionCall.args) : "")).join(""));
559
+ for (const chunk of chunks) delete chunk.usageMetadata;
560
+ chunks[chunks.length - 1].usageMetadata = {
561
+ promptTokenCount: usage.prompt_tokens,
562
+ candidatesTokenCount: usage.completion_tokens,
563
+ totalTokenCount: usage.total_tokens
564
+ };
565
+ return {
566
+ chunks,
567
+ summary: {
568
+ ...plan.summary,
569
+ servedToolCalls
570
+ }
571
+ };
572
+ }
497
573
  async function handleGemini(req, res, raw, model, streaming, fixtures, journal, defaults, setCorsHeaders, providerKey = "gemini") {
498
574
  const { logger } = defaults;
575
+ const wire = providerKey === "vertexai" ? "Vertex AI" : "Gemini";
499
576
  setCorsHeaders(res);
500
577
  let geminiReq;
501
578
  try {
@@ -630,21 +707,101 @@ async function handleGemini(req, res, raw, model, streaming, fixtures, journal,
630
707
  return;
631
708
  }
632
709
  const response = await resolveResponse(fixture, completionReq);
710
+ const misbehavior = planMisbehavior({
711
+ wire: "gemini",
712
+ toolCallIdMode: "authored-nonempty",
713
+ fixture,
714
+ response,
715
+ request: completionReq,
716
+ stream: streaming,
717
+ defaults,
718
+ rawHeaders: req.headers,
719
+ url: req.url
720
+ });
721
+ if (misbehavior.kind === "error") {
722
+ recordMisbehaviorOutcome({
723
+ entry: journal.add({
724
+ method: req.method ?? "POST",
725
+ path,
726
+ headers: flattenHeaders(req.headers),
727
+ body: completionReq,
728
+ response: {
729
+ status: misbehavior.status,
730
+ fixture,
731
+ error: misbehavior.message
732
+ }
733
+ }),
734
+ summary: misbehavior.summary,
735
+ defaults,
736
+ testId
737
+ });
738
+ if (misbehavior.summary?.evaluations.length === 0) logger.error(`${req.method ?? "POST"} ${path}: ${misbehavior.message}`);
739
+ writeErrorResponse(res, misbehavior.status, JSON.stringify({ error: {
740
+ code: misbehavior.status,
741
+ message: misbehavior.message,
742
+ status: misbehavior.status === 400 ? "INVALID_ARGUMENT" : "UNIMPLEMENTED"
743
+ } }));
744
+ return;
745
+ }
633
746
  const latency = fixture.latency ?? defaults.latency;
634
747
  const chunkSize = Math.max(1, fixture.chunkSize ?? defaults.chunkSize);
635
748
  const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;
636
- if (isErrorResponse(response)) {
637
- const status = response.status ?? 500;
638
- journal.add({
749
+ if (misbehavior.kind === "applied") {
750
+ const prepared = prepareGeminiMisbehavior(misbehavior, completionReq, streaming, chunkSize, logger, resolveStrictMode(defaults.strict, req.headers));
751
+ const entry = journal.add({
639
752
  method: req.method ?? "POST",
640
753
  path,
641
754
  headers: flattenHeaders(req.headers),
642
755
  body: completionReq,
643
756
  response: {
644
- status,
757
+ status: 200,
645
758
  fixture
646
759
  }
647
760
  });
761
+ recordMisbehaviorOutcome({
762
+ entry,
763
+ summary: prepared.summary,
764
+ defaults,
765
+ testId
766
+ });
767
+ if (!streaming) {
768
+ res.writeHead(200, { "Content-Type": "application/json" });
769
+ res.end(JSON.stringify(prepared.chunks[0]));
770
+ } else {
771
+ const interruption = createInterruptionSignal(fixture);
772
+ if (!await writeGeminiSSEStream(res, prepared.chunks, {
773
+ latency,
774
+ streamingProfile: fixture.streamingProfile,
775
+ recordedTimings: fixture.recordedTimings,
776
+ replaySpeed,
777
+ signal: interruption?.signal,
778
+ onChunkSent: interruption?.tick
779
+ })) {
780
+ if (!res.writableEnded) res.destroy();
781
+ entry.response.interrupted = true;
782
+ entry.response.interruptReason = interruption?.reason();
783
+ }
784
+ interruption?.cleanup();
785
+ }
786
+ return;
787
+ }
788
+ if (isErrorResponse(response)) {
789
+ const status = response.status ?? 500;
790
+ recordMisbehaviorOutcome({
791
+ entry: journal.add({
792
+ method: req.method ?? "POST",
793
+ path,
794
+ headers: flattenHeaders(req.headers),
795
+ body: completionReq,
796
+ response: {
797
+ status,
798
+ fixture
799
+ }
800
+ }),
801
+ summary: misbehavior.summary,
802
+ defaults,
803
+ testId
804
+ });
648
805
  const geminiError = { error: {
649
806
  code: status,
650
807
  message: response.error.message,
@@ -666,12 +823,19 @@ async function handleGemini(req, res, raw, model, streaming, fixtures, journal,
666
823
  fixture
667
824
  }
668
825
  });
826
+ recordMisbehaviorOutcome({
827
+ entry: journalEntry,
828
+ summary: misbehavior.summary,
829
+ defaults,
830
+ testId
831
+ });
832
+ const audioToolCalls = Array.isArray(response.toolCalls) ? requireServedFunctionToolCalls(response, wire) : [];
669
833
  if (!streaming) {
670
- const body = buildGeminiAudioResponse(response, logger, effReasoning);
834
+ const body = buildGeminiAudioResponse(response, audioToolCalls, logger, effReasoning);
671
835
  res.writeHead(200, { "Content-Type": "application/json" });
672
836
  res.end(JSON.stringify(body));
673
837
  } else {
674
- const chunks = buildGeminiAudioStreamChunks(response, logger, effReasoning);
838
+ const chunks = buildGeminiAudioStreamChunks(response, audioToolCalls, logger, effReasoning);
675
839
  const interruption = createInterruptionSignal(fixture);
676
840
  if (!await writeGeminiSSEStream(res, chunks, {
677
841
  latency,
@@ -689,7 +853,7 @@ async function handleGemini(req, res, raw, model, streaming, fixtures, journal,
689
853
  }
690
854
  return;
691
855
  }
692
- if (isContentWithToolCallsResponse(response)) {
856
+ if (isCombinedFixtureResponse(response)) {
693
857
  if (response.webSearches?.length) logger.warn("webSearches in fixture response are not supported for Gemini API — ignoring");
694
858
  const overrides = extractOverrides(response);
695
859
  const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);
@@ -704,12 +868,19 @@ async function handleGemini(req, res, raw, model, streaming, fixtures, journal,
704
868
  fixture
705
869
  }
706
870
  });
871
+ recordMisbehaviorOutcome({
872
+ entry: journalEntry,
873
+ summary: misbehavior.summary,
874
+ defaults,
875
+ testId
876
+ });
877
+ const functionToolCalls = requireEmittedFunctionToolCalls(response, wire);
707
878
  if (!streaming) {
708
- const body = buildGeminiContentWithToolCallsResponse(response.content ?? "", response.toolCalls ?? [], logger, effReasoning, overrides, response.blocks);
879
+ const body = buildGeminiContentWithToolCallsResponse(response.content ?? "", functionToolCalls, logger, effReasoning, overrides, response.blocks);
709
880
  res.writeHead(200, { "Content-Type": "application/json" });
710
881
  res.end(JSON.stringify(body));
711
882
  } else {
712
- const chunks = buildGeminiContentWithToolCallsStreamChunks(response.content ?? "", response.toolCalls ?? [], chunkSize, logger, effReasoning, overrides, response.blocks);
883
+ const chunks = buildGeminiContentWithToolCallsStreamChunks(response.content ?? "", functionToolCalls, chunkSize, logger, effReasoning, overrides, response.blocks);
713
884
  const interruption = createInterruptionSignal(fixture);
714
885
  if (!await writeGeminiSSEStream(res, chunks, {
715
886
  latency,
@@ -742,6 +913,12 @@ async function handleGemini(req, res, raw, model, streaming, fixtures, journal,
742
913
  fixture
743
914
  }
744
915
  });
916
+ recordMisbehaviorOutcome({
917
+ entry: journalEntry,
918
+ summary: misbehavior.summary,
919
+ defaults,
920
+ testId
921
+ });
745
922
  if (!streaming) {
746
923
  const body = buildGeminiTextResponse(response.content, effReasoning, overrides);
747
924
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -780,12 +957,19 @@ async function handleGemini(req, res, raw, model, streaming, fixtures, journal,
780
957
  fixture
781
958
  }
782
959
  });
960
+ recordMisbehaviorOutcome({
961
+ entry: journalEntry,
962
+ summary: misbehavior.summary,
963
+ defaults,
964
+ testId
965
+ });
966
+ const functionToolCalls = requireEmittedFunctionToolCalls(response, wire);
783
967
  if (!streaming) {
784
- const body = buildGeminiToolCallResponse(response.toolCalls, logger, effReasoning, overrides);
968
+ const body = buildGeminiToolCallResponse(functionToolCalls, logger, effReasoning, overrides);
785
969
  res.writeHead(200, { "Content-Type": "application/json" });
786
970
  res.end(JSON.stringify(body));
787
971
  } else {
788
- const chunks = buildGeminiToolCallStreamChunks(response.toolCalls, chunkSize, logger, effReasoning, overrides);
972
+ const chunks = buildGeminiToolCallStreamChunks(functionToolCalls, chunkSize, logger, effReasoning, overrides);
789
973
  const interruption = createInterruptionSignal(fixture);
790
974
  if (!await writeGeminiSSEStream(res, chunks, {
791
975
  latency,
@@ -803,15 +987,20 @@ async function handleGemini(req, res, raw, model, streaming, fixtures, journal,
803
987
  }
804
988
  return;
805
989
  }
806
- journal.add({
807
- method: req.method ?? "POST",
808
- path,
809
- headers: flattenHeaders(req.headers),
810
- body: completionReq,
811
- response: {
812
- status: 500,
813
- fixture
814
- }
990
+ recordMisbehaviorOutcome({
991
+ entry: journal.add({
992
+ method: req.method ?? "POST",
993
+ path,
994
+ headers: flattenHeaders(req.headers),
995
+ body: completionReq,
996
+ response: {
997
+ status: 500,
998
+ fixture
999
+ }
1000
+ }),
1001
+ summary: misbehavior.summary,
1002
+ defaults,
1003
+ testId
815
1004
  });
816
1005
  writeErrorResponse(res, 500, JSON.stringify({ error: {
817
1006
  message: "Fixture response did not match any known type",