@copilotkit/aimock 1.13.0 → 1.14.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 (85) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +4 -4
  3. package/README.md +6 -1
  4. package/dist/cli.cjs +1 -1
  5. package/dist/cli.cjs.map +1 -1
  6. package/dist/cli.js +1 -1
  7. package/dist/cli.js.map +1 -1
  8. package/dist/config-loader.d.cts.map +1 -1
  9. package/dist/fixture-loader.cjs +131 -29
  10. package/dist/fixture-loader.cjs.map +1 -1
  11. package/dist/fixture-loader.d.cts +9 -2
  12. package/dist/fixture-loader.d.cts.map +1 -1
  13. package/dist/fixture-loader.d.ts +9 -2
  14. package/dist/fixture-loader.d.ts.map +1 -1
  15. package/dist/fixture-loader.js +132 -31
  16. package/dist/fixture-loader.js.map +1 -1
  17. package/dist/gemini.cjs +76 -55
  18. package/dist/gemini.cjs.map +1 -1
  19. package/dist/gemini.d.cts.map +1 -1
  20. package/dist/gemini.d.ts.map +1 -1
  21. package/dist/gemini.js +77 -56
  22. package/dist/gemini.js.map +1 -1
  23. package/dist/helpers.cjs +142 -76
  24. package/dist/helpers.cjs.map +1 -1
  25. package/dist/helpers.d.cts +14 -4
  26. package/dist/helpers.d.cts.map +1 -1
  27. package/dist/helpers.d.ts +14 -4
  28. package/dist/helpers.d.ts.map +1 -1
  29. package/dist/helpers.js +142 -77
  30. package/dist/helpers.js.map +1 -1
  31. package/dist/index.cjs +10 -0
  32. package/dist/index.d.cts +4 -4
  33. package/dist/index.d.ts +4 -4
  34. package/dist/index.js +3 -3
  35. package/dist/llmock.cjs +1 -1
  36. package/dist/llmock.cjs.map +1 -1
  37. package/dist/llmock.d.cts +6 -6
  38. package/dist/llmock.d.cts.map +1 -1
  39. package/dist/llmock.d.ts +6 -6
  40. package/dist/llmock.d.ts.map +1 -1
  41. package/dist/llmock.js +2 -2
  42. package/dist/llmock.js.map +1 -1
  43. package/dist/messages.cjs +69 -63
  44. package/dist/messages.cjs.map +1 -1
  45. package/dist/messages.d.cts.map +1 -1
  46. package/dist/messages.d.ts.map +1 -1
  47. package/dist/messages.js +70 -64
  48. package/dist/messages.js.map +1 -1
  49. package/dist/recorder.cjs +1 -1
  50. package/dist/recorder.cjs.map +1 -1
  51. package/dist/recorder.js +1 -1
  52. package/dist/recorder.js.map +1 -1
  53. package/dist/responses.cjs +66 -57
  54. package/dist/responses.cjs.map +1 -1
  55. package/dist/responses.d.cts +3 -3
  56. package/dist/responses.d.cts.map +1 -1
  57. package/dist/responses.d.ts +3 -3
  58. package/dist/responses.d.ts.map +1 -1
  59. package/dist/responses.js +67 -58
  60. package/dist/responses.js.map +1 -1
  61. package/dist/server.cjs +57 -30
  62. package/dist/server.cjs.map +1 -1
  63. package/dist/server.d.cts.map +1 -1
  64. package/dist/server.d.ts.map +1 -1
  65. package/dist/server.js +58 -31
  66. package/dist/server.js.map +1 -1
  67. package/dist/stream-collapse.cjs.map +1 -1
  68. package/dist/stream-collapse.d.cts.map +1 -1
  69. package/dist/stream-collapse.d.ts.map +1 -1
  70. package/dist/stream-collapse.js.map +1 -1
  71. package/dist/types.d.cts +64 -11
  72. package/dist/types.d.cts.map +1 -1
  73. package/dist/types.d.ts +64 -11
  74. package/dist/types.d.ts.map +1 -1
  75. package/fixtures/example-multi-turn.json +1 -1
  76. package/fixtures/example-tool-call.json +1 -1
  77. package/fixtures/examples/adk/gemini-agent.json +47 -0
  78. package/fixtures/examples/crewai/multi-agent-crew.json +16 -0
  79. package/fixtures/examples/langchain/agent-loop.json +27 -0
  80. package/fixtures/examples/llamaindex/aimock-config.json +62 -0
  81. package/fixtures/examples/llamaindex/rag-pipeline.json +34 -0
  82. package/fixtures/examples/mastra/agent-workflow.json +32 -0
  83. package/fixtures/examples/pydanticai/structured-output.json +15 -0
  84. package/package.json +2 -1
  85. package/skills/write-fixtures/SKILL.md +148 -22
@@ -1,7 +1,14 @@
1
1
  import { Logger } from "./logger.js";
2
- import { Fixture } from "./types.js";
2
+ import { Fixture, FixtureFileResponse, FixtureResponse } from "./types.js";
3
3
 
4
4
  //#region src/fixture-loader.d.ts
5
+
6
+ /**
7
+ * Auto-stringify object-valued `content` and `toolCalls[].arguments` fields.
8
+ * This lets fixture authors write plain JSON objects instead of escaped strings.
9
+ * All other fields (including ResponseOverrides) pass through unmodified.
10
+ */
11
+ declare function normalizeResponse(raw: FixtureFileResponse): FixtureResponse;
5
12
  declare function loadFixtureFile(filePath: string, logger?: Logger): Fixture[];
6
13
  declare function loadFixturesFromDir(dirPath: string, logger?: Logger): Fixture[];
7
14
  interface ValidationResult {
@@ -13,5 +20,5 @@ declare function validateFixtures(fixtures: Fixture[]): ValidationResult[];
13
20
  //# sourceMappingURL=fixture-loader.d.ts.map
14
21
 
15
22
  //#endregion
16
- export { ValidationResult, loadFixtureFile, loadFixturesFromDir, validateFixtures };
23
+ export { ValidationResult, loadFixtureFile, loadFixturesFromDir, normalizeResponse, validateFixtures };
17
24
  //# sourceMappingURL=fixture-loader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fixture-loader.d.ts","names":[],"sources":["../src/fixture-loader.ts"],"sourcesContent":[],"mappings":";;;;iBA2CgB,eAAA,4BAA2C,SAAS;AAApD,iBA6BA,mBAAA,CA7Be,OAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EA6B+B,MA7B/B,CAAA,EA6BwC,OA7BxC,EAAA;AAAA,UAwEd,gBAAA,CAxEc;UAA4B,EAAA,OAAA,GAAA,SAAA;cAAS,EAAA,MAAA;EAAO,OAAA,EAAA,MAAA;AA6B3E;AAAmC,iBAiDnB,gBAAA,CAjDmB,QAAA,EAiDQ,OAjDR,EAAA,CAAA,EAiDoB,gBAjDpB,EAAA"}
1
+ {"version":3,"file":"fixture-loader.d.ts","names":[],"sources":["../src/fixture-loader.ts"],"sourcesContent":[],"mappings":";;;;;;;AA4BA;;;AAA6D,iBAA7C,iBAAA,CAA6C,GAAA,EAAtB,mBAAsB,CAAA,EAAA,eAAA;AAuD7C,iBAAA,eAAA,CAAe,QAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EAA4B,MAA5B,CAAA,EAAqC,OAArC,EAAA;AAAA,iBA6Bf,mBAAA,CA7Be,OAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EA6B+B,MA7B/B,CAAA,EA6BwC,OA7BxC,EAAA;AAA4B,UAwE1C,gBAAA,CAxE0C;UAAS,EAAA,OAAA,GAAA,SAAA;EAAO,YAAA,EAAA,MAAA;EA6B3D,OAAA,EAAA,MAAA;;AAA8C,iBA+G9C,gBAAA,CA/G8C,QAAA,EA+GnB,OA/GmB,EAAA,CAAA,EA+GP,gBA/GO,EAAA"}
@@ -1,8 +1,25 @@
1
- import { isEmbeddingResponse, isErrorResponse, isTextResponse, isToolCallResponse } from "./helpers.js";
1
+ import { isAudioResponse, isContentWithToolCallsResponse, isEmbeddingResponse, isErrorResponse, isImageResponse, isTextResponse, isToolCallResponse, isTranscriptionResponse, isVideoResponse } from "./helpers.js";
2
2
  import { join } from "node:path";
3
3
  import { readFileSync, readdirSync, statSync } from "node:fs";
4
4
 
5
5
  //#region src/fixture-loader.ts
6
+ /**
7
+ * Auto-stringify object-valued `content` and `toolCalls[].arguments` fields.
8
+ * This lets fixture authors write plain JSON objects instead of escaped strings.
9
+ * All other fields (including ResponseOverrides) pass through unmodified.
10
+ */
11
+ function normalizeResponse(raw) {
12
+ const response = { ...raw };
13
+ if (typeof response.content === "object" && response.content !== null) response.content = JSON.stringify(response.content);
14
+ if (Array.isArray(response.toolCalls)) response.toolCalls = response.toolCalls.map((tc) => {
15
+ if (typeof tc.arguments === "object" && tc.arguments !== null) return {
16
+ ...tc,
17
+ arguments: JSON.stringify(tc.arguments)
18
+ };
19
+ return tc;
20
+ });
21
+ return response;
22
+ }
6
23
  function entryToFixture(entry) {
7
24
  return {
8
25
  match: {
@@ -12,9 +29,10 @@ function entryToFixture(entry) {
12
29
  toolName: entry.match.toolName,
13
30
  model: entry.match.model,
14
31
  responseFormat: entry.match.responseFormat,
32
+ endpoint: entry.match.endpoint,
15
33
  ...entry.match.sequenceIndex !== void 0 && { sequenceIndex: entry.match.sequenceIndex }
16
34
  },
17
- response: entry.response,
35
+ response: normalizeResponse(entry.response),
18
36
  ...entry.latency !== void 0 && { latency: entry.latency },
19
37
  ...entry.chunkSize !== void 0 && { chunkSize: entry.chunkSize },
20
38
  ...entry.truncateAfterChunks !== void 0 && { truncateAfterChunks: entry.truncateAfterChunks },
@@ -78,16 +96,57 @@ function loadFixturesFromDir(dirPath, logger) {
78
96
  }
79
97
  return fixtures;
80
98
  }
99
+ function validateReasoning(response, fixtureIndex, results) {
100
+ if (response.reasoning !== void 0) {
101
+ if (typeof response.reasoning !== "string") results.push({
102
+ severity: "error",
103
+ fixtureIndex,
104
+ message: "reasoning must be a string"
105
+ });
106
+ else if (response.reasoning === "") results.push({
107
+ severity: "warning",
108
+ fixtureIndex,
109
+ message: "reasoning is empty string — no reasoning events will be emitted"
110
+ });
111
+ }
112
+ }
113
+ function validateWebSearches(response, fixtureIndex, results) {
114
+ if (response.webSearches !== void 0) if (!Array.isArray(response.webSearches)) results.push({
115
+ severity: "error",
116
+ fixtureIndex,
117
+ message: "webSearches must be an array of strings"
118
+ });
119
+ else if (response.webSearches.length === 0) results.push({
120
+ severity: "warning",
121
+ fixtureIndex,
122
+ message: "webSearches is empty array — no web search events will be emitted"
123
+ });
124
+ else for (let j = 0; j < response.webSearches.length; j++) {
125
+ if (typeof response.webSearches[j] !== "string") {
126
+ results.push({
127
+ severity: "error",
128
+ fixtureIndex,
129
+ message: `webSearches[${j}] is not a string`
130
+ });
131
+ break;
132
+ }
133
+ if (response.webSearches[j] === "") results.push({
134
+ severity: "warning",
135
+ fixtureIndex,
136
+ message: `webSearches[${j}] is empty string`
137
+ });
138
+ }
139
+ }
81
140
  function validateFixtures(fixtures) {
82
141
  const results = [];
83
142
  const seenUserMessages = /* @__PURE__ */ new Map();
84
143
  for (let i = 0; i < fixtures.length; i++) {
85
144
  const f = fixtures[i];
86
145
  const response = f.response;
87
- if (!isTextResponse(response) && !isToolCallResponse(response) && !isErrorResponse(response) && !isEmbeddingResponse(response)) results.push({
146
+ if (!isContentWithToolCallsResponse(response) && !isTextResponse(response) && !isToolCallResponse(response) && !isErrorResponse(response) && !isEmbeddingResponse(response) && !isImageResponse(response) && !isAudioResponse(response) && !isTranscriptionResponse(response) && !isVideoResponse(response)) results.push({
88
147
  severity: "error",
89
148
  fixtureIndex: i,
90
- message: "response is not a recognized type (must have content, toolCalls, error, or embedding)"
149
+ message: "response is not a recognized type (must have content, toolCalls, error, embedding, image, audio, transcription, or video)"
91
150
  });
92
151
  if (isTextResponse(response)) {
93
152
  if (response.content === "") results.push({
@@ -95,43 +154,39 @@ function validateFixtures(fixtures) {
95
154
  fixtureIndex: i,
96
155
  message: "content is empty string"
97
156
  });
98
- if (response.reasoning !== void 0) {
99
- if (typeof response.reasoning !== "string") results.push({
100
- severity: "error",
101
- fixtureIndex: i,
102
- message: "reasoning must be a string"
103
- });
104
- else if (response.reasoning === "") results.push({
105
- severity: "warning",
106
- fixtureIndex: i,
107
- message: "reasoning is empty string — no reasoning events will be emitted"
108
- });
109
- }
110
- if (response.webSearches !== void 0) if (!Array.isArray(response.webSearches)) results.push({
157
+ validateReasoning(response, i, results);
158
+ validateWebSearches(response, i, results);
159
+ }
160
+ if (isContentWithToolCallsResponse(response)) {
161
+ if (response.content === "") results.push({
111
162
  severity: "error",
112
163
  fixtureIndex: i,
113
- message: "webSearches must be an array of strings"
164
+ message: "content is empty string"
114
165
  });
115
- else if (response.webSearches.length === 0) results.push({
166
+ if (response.toolCalls.length === 0) results.push({
116
167
  severity: "warning",
117
168
  fixtureIndex: i,
118
- message: "webSearches is empty array no web search events will be emitted"
169
+ message: "toolCalls array is empty — fixture will never produce tool calls"
119
170
  });
120
- else for (let j = 0; j < response.webSearches.length; j++) {
121
- if (typeof response.webSearches[j] !== "string") {
171
+ for (let j = 0; j < response.toolCalls.length; j++) {
172
+ const tc = response.toolCalls[j];
173
+ if (!tc.name) results.push({
174
+ severity: "error",
175
+ fixtureIndex: i,
176
+ message: `toolCalls[${j}].name is empty`
177
+ });
178
+ try {
179
+ JSON.parse(tc.arguments);
180
+ } catch {
122
181
  results.push({
123
182
  severity: "error",
124
183
  fixtureIndex: i,
125
- message: `webSearches[${j}] is not a string`
184
+ message: `toolCalls[${j}].arguments is not valid JSON: ${tc.arguments}`
126
185
  });
127
- break;
128
186
  }
129
- if (response.webSearches[j] === "") results.push({
130
- severity: "warning",
131
- fixtureIndex: i,
132
- message: `webSearches[${j}] is empty string`
133
- });
134
187
  }
188
+ validateReasoning(response, i, results);
189
+ validateWebSearches(response, i, results);
135
190
  }
136
191
  if (isToolCallResponse(response)) {
137
192
  if (response.toolCalls.length === 0) results.push({
@@ -184,6 +239,52 @@ function validateFixtures(fixtures) {
184
239
  break;
185
240
  }
186
241
  }
242
+ if (isTextResponse(response) || isToolCallResponse(response) || isContentWithToolCallsResponse(response)) {
243
+ const r = response;
244
+ if (r.id !== void 0 && typeof r.id !== "string") results.push({
245
+ severity: "error",
246
+ fixtureIndex: i,
247
+ message: `override "id" must be a string, got ${typeof r.id}`
248
+ });
249
+ if (r.created !== void 0 && (typeof r.created !== "number" || r.created < 0)) results.push({
250
+ severity: "error",
251
+ fixtureIndex: i,
252
+ message: `override "created" must be a non-negative number`
253
+ });
254
+ if (r.model !== void 0 && typeof r.model !== "string") results.push({
255
+ severity: "error",
256
+ fixtureIndex: i,
257
+ message: `override "model" must be a string, got ${typeof r.model}`
258
+ });
259
+ if (r.finishReason !== void 0 && typeof r.finishReason !== "string") results.push({
260
+ severity: "error",
261
+ fixtureIndex: i,
262
+ message: `override "finishReason" must be a string, got ${typeof r.finishReason}`
263
+ });
264
+ if (r.role !== void 0 && typeof r.role !== "string") results.push({
265
+ severity: "error",
266
+ fixtureIndex: i,
267
+ message: `override "role" must be a string, got ${typeof r.role}`
268
+ });
269
+ if (r.systemFingerprint !== void 0 && typeof r.systemFingerprint !== "string") results.push({
270
+ severity: "error",
271
+ fixtureIndex: i,
272
+ message: `override "systemFingerprint" must be a string, got ${typeof r.systemFingerprint}`
273
+ });
274
+ if (r.usage !== void 0) if (typeof r.usage !== "object" || r.usage === null || Array.isArray(r.usage)) results.push({
275
+ severity: "error",
276
+ fixtureIndex: i,
277
+ message: `override "usage" must be an object`
278
+ });
279
+ else for (const key of Object.keys(r.usage)) {
280
+ const val = r.usage[key];
281
+ if (val !== void 0 && typeof val !== "number") results.push({
282
+ severity: "error",
283
+ fixtureIndex: i,
284
+ message: `override "usage.${key}" must be a number, got ${typeof val}`
285
+ });
286
+ }
287
+ }
187
288
  if (f.latency !== void 0 && f.latency < 0) results.push({
188
289
  severity: "error",
189
290
  fixtureIndex: i,
@@ -251,7 +352,7 @@ function validateFixtures(fixtures) {
251
352
  else seenUserMessages.set(um, i);
252
353
  }
253
354
  const match = f.match;
254
- if (!(match.userMessage !== void 0 || match.inputText !== void 0 || match.responseFormat !== void 0 || match.toolCallId !== void 0 || match.toolName !== void 0 || match.model !== void 0 || match.predicate !== void 0) && i < fixtures.length - 1) results.push({
355
+ if (!(match.endpoint !== void 0 || match.userMessage !== void 0 || match.inputText !== void 0 || match.responseFormat !== void 0 || match.toolCallId !== void 0 || match.toolName !== void 0 || match.model !== void 0 || match.predicate !== void 0) && i < fixtures.length - 1) results.push({
255
356
  severity: "warning",
256
357
  fixtureIndex: i,
257
358
  message: `empty match acts as catch-all but is not the last fixture — shadows fixtures ${i + 1}+`
@@ -261,5 +362,5 @@ function validateFixtures(fixtures) {
261
362
  }
262
363
 
263
364
  //#endregion
264
- export { entryToFixture, loadFixtureFile, loadFixturesFromDir, validateFixtures };
365
+ export { entryToFixture, loadFixtureFile, loadFixturesFromDir, normalizeResponse, validateFixtures };
265
366
  //# sourceMappingURL=fixture-loader.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fixture-loader.js","names":[],"sources":["../src/fixture-loader.ts"],"sourcesContent":["import { readFileSync, readdirSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { Fixture, FixtureFile, FixtureFileEntry } from \"./types.js\";\nimport {\n isTextResponse,\n isToolCallResponse,\n isErrorResponse,\n isEmbeddingResponse,\n} from \"./helpers.js\";\nimport type { Logger } from \"./logger.js\";\n\nexport function entryToFixture(entry: FixtureFileEntry): Fixture {\n return {\n match: {\n userMessage: entry.match.userMessage,\n inputText: entry.match.inputText,\n toolCallId: entry.match.toolCallId,\n toolName: entry.match.toolName,\n model: entry.match.model,\n responseFormat: entry.match.responseFormat,\n ...(entry.match.sequenceIndex !== undefined && { sequenceIndex: entry.match.sequenceIndex }),\n },\n response: entry.response,\n ...(entry.latency !== undefined && { latency: entry.latency }),\n ...(entry.chunkSize !== undefined && { chunkSize: entry.chunkSize }),\n ...(entry.truncateAfterChunks !== undefined && {\n truncateAfterChunks: entry.truncateAfterChunks,\n }),\n ...(entry.disconnectAfterMs !== undefined && { disconnectAfterMs: entry.disconnectAfterMs }),\n ...(entry.streamingProfile !== undefined && { streamingProfile: entry.streamingProfile }),\n ...(entry.chaos !== undefined && { chaos: entry.chaos }),\n };\n}\n\n// Logging helper — uses logger if provided, falls back to console.warn.\nfunction warn(logger: Logger | undefined, msg: string, ...rest: unknown[]): void {\n if (logger) {\n logger.warn(msg, ...rest);\n } else {\n console.warn(`[fixture-loader] ${msg}`, ...rest);\n }\n}\n\nexport function loadFixtureFile(filePath: string, logger?: Logger): Fixture[] {\n let raw: string;\n try {\n raw = readFileSync(filePath, \"utf-8\");\n } catch (err) {\n warn(logger, `Could not read file ${filePath}:`, err);\n return [];\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n warn(logger, `Invalid JSON in ${filePath}:`, err);\n return [];\n }\n\n if (\n typeof parsed !== \"object\" ||\n parsed === null ||\n !Array.isArray((parsed as FixtureFile).fixtures)\n ) {\n warn(logger, `Missing or invalid \"fixtures\" array in ${filePath}`);\n return [];\n }\n\n return (parsed as FixtureFile).fixtures.map(entryToFixture);\n}\n\nexport function loadFixturesFromDir(dirPath: string, logger?: Logger): Fixture[] {\n let entries: string[];\n try {\n entries = readdirSync(dirPath);\n } catch (err) {\n warn(logger, `Could not read directory ${dirPath}:`, err);\n return [];\n }\n\n const jsonFiles: string[] = [];\n for (const name of entries) {\n const fullPath = join(dirPath, name);\n try {\n if (statSync(fullPath).isDirectory()) {\n warn(logger, `Skipping subdirectory ${fullPath} (fixtures are not loaded recursively)`);\n continue;\n }\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code !== \"ENOENT\") {\n warn(logger, `Could not stat ${fullPath}:`, err);\n }\n continue;\n }\n if (name.endsWith(\".json\")) {\n jsonFiles.push(name);\n }\n }\n jsonFiles.sort();\n\n const fixtures: Fixture[] = [];\n for (const name of jsonFiles) {\n const filePath = join(dirPath, name);\n fixtures.push(...loadFixtureFile(filePath, logger));\n }\n\n return fixtures;\n}\n\n// ---------------------------------------------------------------------------\n// Fixture validation\n// ---------------------------------------------------------------------------\n\nexport interface ValidationResult {\n severity: \"error\" | \"warning\";\n fixtureIndex: number;\n message: string;\n}\n\nexport function validateFixtures(fixtures: Fixture[]): ValidationResult[] {\n const results: ValidationResult[] = [];\n\n const seenUserMessages = new Map<string, number>();\n\n for (let i = 0; i < fixtures.length; i++) {\n const f = fixtures[i];\n const response = f.response;\n\n // --- Error checks ---\n\n // Response type recognition\n if (\n !isTextResponse(response) &&\n !isToolCallResponse(response) &&\n !isErrorResponse(response) &&\n !isEmbeddingResponse(response)\n ) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message:\n \"response is not a recognized type (must have content, toolCalls, error, or embedding)\",\n });\n }\n\n // Text response checks\n if (isTextResponse(response)) {\n if (response.content === \"\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"content is empty string\",\n });\n }\n if (response.reasoning !== undefined) {\n if (typeof response.reasoning !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"reasoning must be a string\",\n });\n } else if (response.reasoning === \"\") {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: \"reasoning is empty string — no reasoning events will be emitted\",\n });\n }\n }\n if (response.webSearches !== undefined) {\n if (!Array.isArray(response.webSearches)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"webSearches must be an array of strings\",\n });\n } else if (response.webSearches.length === 0) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: \"webSearches is empty array — no web search events will be emitted\",\n });\n } else {\n for (let j = 0; j < response.webSearches.length; j++) {\n if (typeof response.webSearches[j] !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `webSearches[${j}] is not a string`,\n });\n break;\n }\n if (response.webSearches[j] === \"\") {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: `webSearches[${j}] is empty string`,\n });\n }\n }\n }\n }\n }\n\n // Tool call response checks\n if (isToolCallResponse(response)) {\n if (response.toolCalls.length === 0) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: \"toolCalls array is empty — fixture will never produce tool calls\",\n });\n }\n for (let j = 0; j < response.toolCalls.length; j++) {\n const tc = response.toolCalls[j];\n if (!tc.name) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `toolCalls[${j}].name is empty`,\n });\n }\n try {\n JSON.parse(tc.arguments);\n } catch {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `toolCalls[${j}].arguments is not valid JSON: ${tc.arguments}`,\n });\n }\n }\n }\n\n // Error response checks\n if (isErrorResponse(response)) {\n if (!response.error.message) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"error.message is empty\",\n });\n }\n if (response.status !== undefined && (response.status < 100 || response.status > 599)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `error status ${response.status} is not a valid HTTP status code`,\n });\n }\n }\n\n // Embedding response checks\n if (isEmbeddingResponse(response)) {\n if (response.embedding.length === 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"embedding array is empty\",\n });\n }\n for (let j = 0; j < response.embedding.length; j++) {\n if (typeof response.embedding[j] !== \"number\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `embedding[${j}] is not a number`,\n });\n break; // one error is enough\n }\n }\n }\n\n // Numeric sanity checks\n if (f.latency !== undefined && f.latency < 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"latency must be >= 0\",\n });\n }\n if (f.chunkSize !== undefined && f.chunkSize < 1) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chunkSize must be >= 1\",\n });\n }\n if (f.truncateAfterChunks !== undefined && f.truncateAfterChunks < 1) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"truncateAfterChunks must be >= 1\",\n });\n }\n if (f.disconnectAfterMs !== undefined && f.disconnectAfterMs < 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"disconnectAfterMs must be >= 0\",\n });\n }\n if (f.streamingProfile !== undefined) {\n const sp = f.streamingProfile;\n if (sp.ttft !== undefined && sp.ttft < 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"streamingProfile.ttft must be >= 0\",\n });\n }\n if (sp.tps !== undefined && sp.tps <= 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"streamingProfile.tps must be > 0\",\n });\n }\n if (sp.jitter !== undefined && (sp.jitter < 0 || sp.jitter > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"streamingProfile.jitter must be between 0 and 1\",\n });\n }\n }\n if (f.chaos !== undefined) {\n const ch = f.chaos;\n if (ch.dropRate !== undefined && (ch.dropRate < 0 || ch.dropRate > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chaos.dropRate must be between 0 and 1\",\n });\n }\n if (ch.malformedRate !== undefined && (ch.malformedRate < 0 || ch.malformedRate > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chaos.malformedRate must be between 0 and 1\",\n });\n }\n if (ch.disconnectRate !== undefined && (ch.disconnectRate < 0 || ch.disconnectRate > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chaos.disconnectRate must be between 0 and 1\",\n });\n }\n }\n\n // --- Warning checks ---\n\n // Duplicate userMessage shadowing\n const um = f.match.userMessage;\n if (typeof um === \"string\" && um) {\n const prev = seenUserMessages.get(um);\n if (prev !== undefined) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: `duplicate userMessage '${um}' — shadows fixture ${prev}`,\n });\n } else {\n seenUserMessages.set(um, i);\n }\n }\n\n // Catch-all not in last position\n const match = f.match;\n const hasDiscriminator =\n match.userMessage !== undefined ||\n match.inputText !== undefined ||\n match.responseFormat !== undefined ||\n match.toolCallId !== undefined ||\n match.toolName !== undefined ||\n match.model !== undefined ||\n match.predicate !== undefined;\n\n if (!hasDiscriminator && i < fixtures.length - 1) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: `empty match acts as catch-all but is not the last fixture — shadows fixtures ${i + 1}+`,\n });\n }\n }\n\n return results;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"fixture-loader.js","names":[],"sources":["../src/fixture-loader.ts"],"sourcesContent":["import { readFileSync, readdirSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type {\n Fixture,\n FixtureFile,\n FixtureFileEntry,\n FixtureFileResponse,\n FixtureResponse,\n ResponseOverrides,\n} from \"./types.js\";\nimport {\n isTextResponse,\n isToolCallResponse,\n isContentWithToolCallsResponse,\n isErrorResponse,\n isEmbeddingResponse,\n isImageResponse,\n isAudioResponse,\n isTranscriptionResponse,\n isVideoResponse,\n} from \"./helpers.js\";\nimport type { Logger } from \"./logger.js\";\n\n/**\n * Auto-stringify object-valued `content` and `toolCalls[].arguments` fields.\n * This lets fixture authors write plain JSON objects instead of escaped strings.\n * All other fields (including ResponseOverrides) pass through unmodified.\n */\nexport function normalizeResponse(raw: FixtureFileResponse): FixtureResponse {\n // Shallow-clone so we don't mutate the parsed JSON input.\n const response = { ...raw } as Record<string, unknown>;\n\n // Auto-stringify object content (e.g. structured output)\n if (typeof response.content === \"object\" && response.content !== null) {\n response.content = JSON.stringify(response.content);\n }\n\n // Auto-stringify object arguments in toolCalls\n if (Array.isArray(response.toolCalls)) {\n response.toolCalls = (response.toolCalls as Array<Record<string, unknown>>).map((tc) => {\n if (typeof tc.arguments === \"object\" && tc.arguments !== null) {\n return { ...tc, arguments: JSON.stringify(tc.arguments) };\n }\n return tc;\n });\n }\n\n return response as unknown as FixtureResponse;\n}\n\nexport function entryToFixture(entry: FixtureFileEntry): Fixture {\n return {\n match: {\n userMessage: entry.match.userMessage,\n inputText: entry.match.inputText,\n toolCallId: entry.match.toolCallId,\n toolName: entry.match.toolName,\n model: entry.match.model,\n responseFormat: entry.match.responseFormat,\n endpoint: entry.match.endpoint,\n ...(entry.match.sequenceIndex !== undefined && { sequenceIndex: entry.match.sequenceIndex }),\n },\n response: normalizeResponse(entry.response),\n ...(entry.latency !== undefined && { latency: entry.latency }),\n ...(entry.chunkSize !== undefined && { chunkSize: entry.chunkSize }),\n ...(entry.truncateAfterChunks !== undefined && {\n truncateAfterChunks: entry.truncateAfterChunks,\n }),\n ...(entry.disconnectAfterMs !== undefined && { disconnectAfterMs: entry.disconnectAfterMs }),\n ...(entry.streamingProfile !== undefined && { streamingProfile: entry.streamingProfile }),\n ...(entry.chaos !== undefined && { chaos: entry.chaos }),\n };\n}\n\n// Logging helper — uses logger if provided, falls back to console.warn.\nfunction warn(logger: Logger | undefined, msg: string, ...rest: unknown[]): void {\n if (logger) {\n logger.warn(msg, ...rest);\n } else {\n console.warn(`[fixture-loader] ${msg}`, ...rest);\n }\n}\n\nexport function loadFixtureFile(filePath: string, logger?: Logger): Fixture[] {\n let raw: string;\n try {\n raw = readFileSync(filePath, \"utf-8\");\n } catch (err) {\n warn(logger, `Could not read file ${filePath}:`, err);\n return [];\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n warn(logger, `Invalid JSON in ${filePath}:`, err);\n return [];\n }\n\n if (\n typeof parsed !== \"object\" ||\n parsed === null ||\n !Array.isArray((parsed as FixtureFile).fixtures)\n ) {\n warn(logger, `Missing or invalid \"fixtures\" array in ${filePath}`);\n return [];\n }\n\n return (parsed as FixtureFile).fixtures.map(entryToFixture);\n}\n\nexport function loadFixturesFromDir(dirPath: string, logger?: Logger): Fixture[] {\n let entries: string[];\n try {\n entries = readdirSync(dirPath);\n } catch (err) {\n warn(logger, `Could not read directory ${dirPath}:`, err);\n return [];\n }\n\n const jsonFiles: string[] = [];\n for (const name of entries) {\n const fullPath = join(dirPath, name);\n try {\n if (statSync(fullPath).isDirectory()) {\n warn(logger, `Skipping subdirectory ${fullPath} (fixtures are not loaded recursively)`);\n continue;\n }\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code !== \"ENOENT\") {\n warn(logger, `Could not stat ${fullPath}:`, err);\n }\n continue;\n }\n if (name.endsWith(\".json\")) {\n jsonFiles.push(name);\n }\n }\n jsonFiles.sort();\n\n const fixtures: Fixture[] = [];\n for (const name of jsonFiles) {\n const filePath = join(dirPath, name);\n fixtures.push(...loadFixtureFile(filePath, logger));\n }\n\n return fixtures;\n}\n\n// ---------------------------------------------------------------------------\n// Fixture validation\n// ---------------------------------------------------------------------------\n\nexport interface ValidationResult {\n severity: \"error\" | \"warning\";\n fixtureIndex: number;\n message: string;\n}\n\nfunction validateReasoning(\n response: { reasoning?: unknown },\n fixtureIndex: number,\n results: ValidationResult[],\n): void {\n if (response.reasoning !== undefined) {\n if (typeof response.reasoning !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: \"reasoning must be a string\",\n });\n } else if (response.reasoning === \"\") {\n results.push({\n severity: \"warning\",\n fixtureIndex,\n message: \"reasoning is empty string — no reasoning events will be emitted\",\n });\n }\n }\n}\n\nfunction validateWebSearches(\n response: { webSearches?: unknown },\n fixtureIndex: number,\n results: ValidationResult[],\n): void {\n if (response.webSearches !== undefined) {\n if (!Array.isArray(response.webSearches)) {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: \"webSearches must be an array of strings\",\n });\n } else if (response.webSearches.length === 0) {\n results.push({\n severity: \"warning\",\n fixtureIndex,\n message: \"webSearches is empty array — no web search events will be emitted\",\n });\n } else {\n for (let j = 0; j < response.webSearches.length; j++) {\n if (typeof response.webSearches[j] !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: `webSearches[${j}] is not a string`,\n });\n break;\n }\n if (response.webSearches[j] === \"\") {\n results.push({\n severity: \"warning\",\n fixtureIndex,\n message: `webSearches[${j}] is empty string`,\n });\n }\n }\n }\n }\n}\n\nexport function validateFixtures(fixtures: Fixture[]): ValidationResult[] {\n const results: ValidationResult[] = [];\n\n const seenUserMessages = new Map<string, number>();\n\n for (let i = 0; i < fixtures.length; i++) {\n const f = fixtures[i];\n const response = f.response;\n\n // --- Error checks ---\n\n // Response type recognition\n // Note: isContentWithToolCallsResponse must be checked before isTextResponse\n // and isToolCallResponse since it is a structural superset of both.\n if (\n !isContentWithToolCallsResponse(response) &&\n !isTextResponse(response) &&\n !isToolCallResponse(response) &&\n !isErrorResponse(response) &&\n !isEmbeddingResponse(response) &&\n !isImageResponse(response) &&\n !isAudioResponse(response) &&\n !isTranscriptionResponse(response) &&\n !isVideoResponse(response)\n ) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message:\n \"response is not a recognized type (must have content, toolCalls, error, embedding, image, audio, transcription, or video)\",\n });\n }\n\n // Text response checks\n if (isTextResponse(response)) {\n if (response.content === \"\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"content is empty string\",\n });\n }\n validateReasoning(response, i, results);\n validateWebSearches(response, i, results);\n }\n\n // ContentWithToolCalls response checks\n if (isContentWithToolCallsResponse(response)) {\n if (response.content === \"\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"content is empty string\",\n });\n }\n if (response.toolCalls.length === 0) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: \"toolCalls array is empty — fixture will never produce tool calls\",\n });\n }\n for (let j = 0; j < response.toolCalls.length; j++) {\n const tc = response.toolCalls[j];\n if (!tc.name) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `toolCalls[${j}].name is empty`,\n });\n }\n try {\n JSON.parse(tc.arguments);\n } catch {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `toolCalls[${j}].arguments is not valid JSON: ${tc.arguments}`,\n });\n }\n }\n validateReasoning(response, i, results);\n validateWebSearches(response, i, results);\n }\n\n // Tool call response checks\n if (isToolCallResponse(response)) {\n if (response.toolCalls.length === 0) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: \"toolCalls array is empty — fixture will never produce tool calls\",\n });\n }\n for (let j = 0; j < response.toolCalls.length; j++) {\n const tc = response.toolCalls[j];\n if (!tc.name) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `toolCalls[${j}].name is empty`,\n });\n }\n try {\n JSON.parse(tc.arguments);\n } catch {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `toolCalls[${j}].arguments is not valid JSON: ${tc.arguments}`,\n });\n }\n }\n }\n\n // Error response checks\n if (isErrorResponse(response)) {\n if (!response.error.message) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"error.message is empty\",\n });\n }\n if (response.status !== undefined && (response.status < 100 || response.status > 599)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `error status ${response.status} is not a valid HTTP status code`,\n });\n }\n }\n\n // Embedding response checks\n if (isEmbeddingResponse(response)) {\n if (response.embedding.length === 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"embedding array is empty\",\n });\n }\n for (let j = 0; j < response.embedding.length; j++) {\n if (typeof response.embedding[j] !== \"number\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `embedding[${j}] is not a number`,\n });\n break; // one error is enough\n }\n }\n }\n\n // Validate ResponseOverrides fields\n if (\n isTextResponse(response) ||\n isToolCallResponse(response) ||\n isContentWithToolCallsResponse(response)\n ) {\n const r = response as ResponseOverrides;\n if (r.id !== undefined && typeof r.id !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"id\" must be a string, got ${typeof r.id}`,\n });\n }\n if (r.created !== undefined && (typeof r.created !== \"number\" || r.created < 0)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"created\" must be a non-negative number`,\n });\n }\n if (r.model !== undefined && typeof r.model !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"model\" must be a string, got ${typeof r.model}`,\n });\n }\n if (r.finishReason !== undefined && typeof r.finishReason !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"finishReason\" must be a string, got ${typeof r.finishReason}`,\n });\n }\n if (r.role !== undefined && typeof r.role !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"role\" must be a string, got ${typeof r.role}`,\n });\n }\n if (r.systemFingerprint !== undefined && typeof r.systemFingerprint !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"systemFingerprint\" must be a string, got ${typeof r.systemFingerprint}`,\n });\n }\n if (r.usage !== undefined) {\n if (typeof r.usage !== \"object\" || r.usage === null || Array.isArray(r.usage)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"usage\" must be an object`,\n });\n } else {\n // Check all known usage fields are numbers if present\n for (const key of Object.keys(r.usage)) {\n const val = (r.usage as Record<string, unknown>)[key];\n if (val !== undefined && typeof val !== \"number\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"usage.${key}\" must be a number, got ${typeof val}`,\n });\n }\n }\n }\n }\n }\n\n // Numeric sanity checks\n if (f.latency !== undefined && f.latency < 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"latency must be >= 0\",\n });\n }\n if (f.chunkSize !== undefined && f.chunkSize < 1) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chunkSize must be >= 1\",\n });\n }\n if (f.truncateAfterChunks !== undefined && f.truncateAfterChunks < 1) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"truncateAfterChunks must be >= 1\",\n });\n }\n if (f.disconnectAfterMs !== undefined && f.disconnectAfterMs < 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"disconnectAfterMs must be >= 0\",\n });\n }\n if (f.streamingProfile !== undefined) {\n const sp = f.streamingProfile;\n if (sp.ttft !== undefined && sp.ttft < 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"streamingProfile.ttft must be >= 0\",\n });\n }\n if (sp.tps !== undefined && sp.tps <= 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"streamingProfile.tps must be > 0\",\n });\n }\n if (sp.jitter !== undefined && (sp.jitter < 0 || sp.jitter > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"streamingProfile.jitter must be between 0 and 1\",\n });\n }\n }\n if (f.chaos !== undefined) {\n const ch = f.chaos;\n if (ch.dropRate !== undefined && (ch.dropRate < 0 || ch.dropRate > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chaos.dropRate must be between 0 and 1\",\n });\n }\n if (ch.malformedRate !== undefined && (ch.malformedRate < 0 || ch.malformedRate > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chaos.malformedRate must be between 0 and 1\",\n });\n }\n if (ch.disconnectRate !== undefined && (ch.disconnectRate < 0 || ch.disconnectRate > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chaos.disconnectRate must be between 0 and 1\",\n });\n }\n }\n\n // --- Warning checks ---\n\n // Duplicate userMessage shadowing\n const um = f.match.userMessage;\n if (typeof um === \"string\" && um) {\n const prev = seenUserMessages.get(um);\n if (prev !== undefined) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: `duplicate userMessage '${um}' — shadows fixture ${prev}`,\n });\n } else {\n seenUserMessages.set(um, i);\n }\n }\n\n // Catch-all not in last position\n const match = f.match;\n const hasDiscriminator =\n match.endpoint !== undefined ||\n match.userMessage !== undefined ||\n match.inputText !== undefined ||\n match.responseFormat !== undefined ||\n match.toolCallId !== undefined ||\n match.toolName !== undefined ||\n match.model !== undefined ||\n match.predicate !== undefined;\n\n if (!hasDiscriminator && i < fixtures.length - 1) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: `empty match acts as catch-all but is not the last fixture — shadows fixtures ${i + 1}+`,\n });\n }\n }\n\n return results;\n}\n"],"mappings":"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"}
package/dist/gemini.cjs CHANGED
@@ -84,8 +84,32 @@ function geminiToCompletionRequest(req, model, stream) {
84
84
  tools
85
85
  };
86
86
  }
87
- function buildGeminiTextStreamChunks(content, chunkSize, reasoning) {
87
+ function geminiFinishReason(finishReason, defaultReason) {
88
+ if (!finishReason) return defaultReason;
89
+ if (finishReason === "stop") return "STOP";
90
+ if (finishReason === "tool_calls") return "FUNCTION_CALL";
91
+ if (finishReason === "length") return "MAX_TOKENS";
92
+ if (finishReason === "content_filter") return "SAFETY";
93
+ return finishReason;
94
+ }
95
+ function geminiUsageMetadata(overrides) {
96
+ if (!overrides?.usage) return {
97
+ promptTokenCount: 0,
98
+ candidatesTokenCount: 0,
99
+ totalTokenCount: 0
100
+ };
101
+ const prompt = overrides.usage.promptTokenCount ?? 0;
102
+ const candidates = overrides.usage.candidatesTokenCount ?? 0;
103
+ return {
104
+ promptTokenCount: prompt,
105
+ candidatesTokenCount: candidates,
106
+ totalTokenCount: overrides.usage.totalTokenCount ?? prompt + candidates
107
+ };
108
+ }
109
+ function buildGeminiTextStreamChunks(content, chunkSize, reasoning, overrides) {
88
110
  const chunks = [];
111
+ const effectiveFinish = geminiFinishReason(overrides?.finishReason, "STOP");
112
+ const usage = geminiUsageMetadata(overrides);
89
113
  if (reasoning) for (let i = 0; i < reasoning.length; i += chunkSize) {
90
114
  const slice = reasoning.slice(i, i + chunkSize);
91
115
  chunks.push({ candidates: [{
@@ -109,13 +133,9 @@ function buildGeminiTextStreamChunks(content, chunkSize, reasoning) {
109
133
  parts: [{ text: slice }]
110
134
  },
111
135
  index: 0,
112
- ...isLast ? { finishReason: "STOP" } : {}
136
+ ...isLast ? { finishReason: effectiveFinish } : {}
113
137
  }],
114
- ...isLast ? { usageMetadata: {
115
- promptTokenCount: 0,
116
- candidatesTokenCount: 0,
117
- totalTokenCount: 0
118
- } } : {}
138
+ ...isLast ? { usageMetadata: usage } : {}
119
139
  };
120
140
  chunks.push(chunk);
121
141
  }
@@ -125,14 +145,10 @@ function buildGeminiTextStreamChunks(content, chunkSize, reasoning) {
125
145
  role: "model",
126
146
  parts: [{ text: "" }]
127
147
  },
128
- finishReason: "STOP",
148
+ finishReason: effectiveFinish,
129
149
  index: 0
130
150
  }],
131
- usageMetadata: {
132
- promptTokenCount: 0,
133
- candidatesTokenCount: 0,
134
- totalTokenCount: 0
135
- }
151
+ usageMetadata: usage
136
152
  });
137
153
  return chunks;
138
154
  }
@@ -150,24 +166,20 @@ function parseToolCallPart(tc, logger) {
150
166
  id: tc.id || require_helpers.generateToolCallId()
151
167
  } };
152
168
  }
153
- function buildGeminiToolCallStreamChunks(toolCalls, logger) {
169
+ function buildGeminiToolCallStreamChunks(toolCalls, logger, overrides) {
154
170
  return [{
155
171
  candidates: [{
156
172
  content: {
157
173
  role: "model",
158
174
  parts: toolCalls.map((tc) => parseToolCallPart(tc, logger))
159
175
  },
160
- finishReason: "FUNCTION_CALL",
176
+ finishReason: geminiFinishReason(overrides?.finishReason, "FUNCTION_CALL"),
161
177
  index: 0
162
178
  }],
163
- usageMetadata: {
164
- promptTokenCount: 0,
165
- candidatesTokenCount: 0,
166
- totalTokenCount: 0
167
- }
179
+ usageMetadata: geminiUsageMetadata(overrides)
168
180
  }];
169
181
  }
170
- function buildGeminiTextResponse(content, reasoning) {
182
+ function buildGeminiTextResponse(content, reasoning, overrides) {
171
183
  const parts = [];
172
184
  if (reasoning) parts.push({
173
185
  text: reasoning,
@@ -180,35 +192,40 @@ function buildGeminiTextResponse(content, reasoning) {
180
192
  role: "model",
181
193
  parts
182
194
  },
183
- finishReason: "STOP",
195
+ finishReason: geminiFinishReason(overrides?.finishReason, "STOP"),
184
196
  index: 0
185
197
  }],
186
- usageMetadata: {
187
- promptTokenCount: 0,
188
- candidatesTokenCount: 0,
189
- totalTokenCount: 0
190
- }
198
+ usageMetadata: geminiUsageMetadata(overrides)
191
199
  };
192
200
  }
193
- function buildGeminiToolCallResponse(toolCalls, logger) {
201
+ function buildGeminiToolCallResponse(toolCalls, logger, overrides) {
194
202
  return {
195
203
  candidates: [{
196
204
  content: {
197
205
  role: "model",
198
206
  parts: toolCalls.map((tc) => parseToolCallPart(tc, logger))
199
207
  },
200
- finishReason: "FUNCTION_CALL",
208
+ finishReason: geminiFinishReason(overrides?.finishReason, "FUNCTION_CALL"),
201
209
  index: 0
202
210
  }],
203
- usageMetadata: {
204
- promptTokenCount: 0,
205
- candidatesTokenCount: 0,
206
- totalTokenCount: 0
207
- }
211
+ usageMetadata: geminiUsageMetadata(overrides)
208
212
  };
209
213
  }
210
- function buildGeminiContentWithToolCallsStreamChunks(content, toolCalls, chunkSize, logger) {
214
+ function buildGeminiContentWithToolCallsStreamChunks(content, toolCalls, chunkSize, logger, reasoning, overrides) {
211
215
  const chunks = [];
216
+ if (reasoning) for (let i = 0; i < reasoning.length; i += chunkSize) {
217
+ const slice = reasoning.slice(i, i + chunkSize);
218
+ chunks.push({ candidates: [{
219
+ content: {
220
+ role: "model",
221
+ parts: [{
222
+ text: slice,
223
+ thought: true
224
+ }]
225
+ },
226
+ index: 0
227
+ }] });
228
+ }
212
229
  if (content.length === 0) chunks.push({ candidates: [{
213
230
  content: {
214
231
  role: "model",
@@ -233,32 +250,31 @@ function buildGeminiContentWithToolCallsStreamChunks(content, toolCalls, chunkSi
233
250
  role: "model",
234
251
  parts
235
252
  },
236
- finishReason: "FUNCTION_CALL",
253
+ finishReason: geminiFinishReason(overrides?.finishReason, "FUNCTION_CALL"),
237
254
  index: 0
238
255
  }],
239
- usageMetadata: {
240
- promptTokenCount: 0,
241
- candidatesTokenCount: 0,
242
- totalTokenCount: 0
243
- }
256
+ usageMetadata: geminiUsageMetadata(overrides)
244
257
  });
245
258
  return chunks;
246
259
  }
247
- function buildGeminiContentWithToolCallsResponse(content, toolCalls, logger) {
260
+ function buildGeminiContentWithToolCallsResponse(content, toolCalls, logger, reasoning, overrides) {
261
+ const parts = [];
262
+ if (reasoning) parts.push({
263
+ text: reasoning,
264
+ thought: true
265
+ });
266
+ parts.push({ text: content });
267
+ parts.push(...toolCalls.map((tc) => parseToolCallPart(tc, logger)));
248
268
  return {
249
269
  candidates: [{
250
270
  content: {
251
271
  role: "model",
252
- parts: [{ text: content }, ...toolCalls.map((tc) => parseToolCallPart(tc, logger))]
272
+ parts
253
273
  },
254
- finishReason: "FUNCTION_CALL",
274
+ finishReason: geminiFinishReason(overrides?.finishReason, "FUNCTION_CALL"),
255
275
  index: 0
256
276
  }],
257
- usageMetadata: {
258
- promptTokenCount: 0,
259
- candidatesTokenCount: 0,
260
- totalTokenCount: 0
261
- }
277
+ usageMetadata: geminiUsageMetadata(overrides)
262
278
  };
263
279
  }
264
280
  async function writeGeminiSSEStream(res, chunks, optionsOrLatency) {
@@ -381,6 +397,8 @@ async function handleGemini(req, res, raw, model, streaming, fixtures, journal,
381
397
  return;
382
398
  }
383
399
  if (require_helpers.isContentWithToolCallsResponse(response)) {
400
+ if (response.webSearches?.length) logger.warn("webSearches in fixture response are not supported for Gemini API — ignoring");
401
+ const overrides = require_helpers.extractOverrides(response);
384
402
  const journalEntry = journal.add({
385
403
  method: req.method ?? "POST",
386
404
  path,
@@ -392,11 +410,11 @@ async function handleGemini(req, res, raw, model, streaming, fixtures, journal,
392
410
  }
393
411
  });
394
412
  if (!streaming) {
395
- const body = buildGeminiContentWithToolCallsResponse(response.content, response.toolCalls, logger);
413
+ const body = buildGeminiContentWithToolCallsResponse(response.content, response.toolCalls, logger, response.reasoning, overrides);
396
414
  res.writeHead(200, { "Content-Type": "application/json" });
397
415
  res.end(JSON.stringify(body));
398
416
  } else {
399
- const chunks = buildGeminiContentWithToolCallsStreamChunks(response.content, response.toolCalls, chunkSize, logger);
417
+ const chunks = buildGeminiContentWithToolCallsStreamChunks(response.content, response.toolCalls, chunkSize, logger, response.reasoning, overrides);
400
418
  const interruption = require_interruption.createInterruptionSignal(fixture);
401
419
  if (!await writeGeminiSSEStream(res, chunks, {
402
420
  latency,
@@ -413,6 +431,8 @@ async function handleGemini(req, res, raw, model, streaming, fixtures, journal,
413
431
  return;
414
432
  }
415
433
  if (require_helpers.isTextResponse(response)) {
434
+ if (response.webSearches?.length) logger.warn("webSearches in fixture response are not supported for Gemini API — ignoring");
435
+ const overrides = require_helpers.extractOverrides(response);
416
436
  const journalEntry = journal.add({
417
437
  method: req.method ?? "POST",
418
438
  path,
@@ -424,11 +444,11 @@ async function handleGemini(req, res, raw, model, streaming, fixtures, journal,
424
444
  }
425
445
  });
426
446
  if (!streaming) {
427
- const body = buildGeminiTextResponse(response.content, response.reasoning);
447
+ const body = buildGeminiTextResponse(response.content, response.reasoning, overrides);
428
448
  res.writeHead(200, { "Content-Type": "application/json" });
429
449
  res.end(JSON.stringify(body));
430
450
  } else {
431
- const chunks = buildGeminiTextStreamChunks(response.content, chunkSize, response.reasoning);
451
+ const chunks = buildGeminiTextStreamChunks(response.content, chunkSize, response.reasoning, overrides);
432
452
  const interruption = require_interruption.createInterruptionSignal(fixture);
433
453
  if (!await writeGeminiSSEStream(res, chunks, {
434
454
  latency,
@@ -445,6 +465,7 @@ async function handleGemini(req, res, raw, model, streaming, fixtures, journal,
445
465
  return;
446
466
  }
447
467
  if (require_helpers.isToolCallResponse(response)) {
468
+ const overrides = require_helpers.extractOverrides(response);
448
469
  const journalEntry = journal.add({
449
470
  method: req.method ?? "POST",
450
471
  path,
@@ -456,11 +477,11 @@ async function handleGemini(req, res, raw, model, streaming, fixtures, journal,
456
477
  }
457
478
  });
458
479
  if (!streaming) {
459
- const body = buildGeminiToolCallResponse(response.toolCalls, logger);
480
+ const body = buildGeminiToolCallResponse(response.toolCalls, logger, overrides);
460
481
  res.writeHead(200, { "Content-Type": "application/json" });
461
482
  res.end(JSON.stringify(body));
462
483
  } else {
463
- const chunks = buildGeminiToolCallStreamChunks(response.toolCalls, logger);
484
+ const chunks = buildGeminiToolCallStreamChunks(response.toolCalls, logger, overrides);
464
485
  const interruption = require_interruption.createInterruptionSignal(fixture);
465
486
  if (!await writeGeminiSSEStream(res, chunks, {
466
487
  latency,