@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +4 -4
- package/README.md +6 -1
- package/dist/cli.cjs +1 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/config-loader.d.cts.map +1 -1
- package/dist/fixture-loader.cjs +131 -29
- package/dist/fixture-loader.cjs.map +1 -1
- package/dist/fixture-loader.d.cts +9 -2
- package/dist/fixture-loader.d.cts.map +1 -1
- package/dist/fixture-loader.d.ts +9 -2
- package/dist/fixture-loader.d.ts.map +1 -1
- package/dist/fixture-loader.js +132 -31
- package/dist/fixture-loader.js.map +1 -1
- package/dist/gemini.cjs +76 -55
- package/dist/gemini.cjs.map +1 -1
- package/dist/gemini.d.cts.map +1 -1
- package/dist/gemini.d.ts.map +1 -1
- package/dist/gemini.js +77 -56
- package/dist/gemini.js.map +1 -1
- package/dist/helpers.cjs +142 -76
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts +14 -4
- package/dist/helpers.d.cts.map +1 -1
- package/dist/helpers.d.ts +14 -4
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +142 -77
- package/dist/helpers.js.map +1 -1
- package/dist/index.cjs +10 -0
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/llmock.cjs +1 -1
- package/dist/llmock.cjs.map +1 -1
- package/dist/llmock.d.cts +6 -6
- package/dist/llmock.d.cts.map +1 -1
- package/dist/llmock.d.ts +6 -6
- package/dist/llmock.d.ts.map +1 -1
- package/dist/llmock.js +2 -2
- package/dist/llmock.js.map +1 -1
- package/dist/messages.cjs +69 -63
- package/dist/messages.cjs.map +1 -1
- package/dist/messages.d.cts.map +1 -1
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +70 -64
- package/dist/messages.js.map +1 -1
- package/dist/recorder.cjs +1 -1
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.js +1 -1
- package/dist/recorder.js.map +1 -1
- package/dist/responses.cjs +66 -57
- package/dist/responses.cjs.map +1 -1
- package/dist/responses.d.cts +3 -3
- package/dist/responses.d.cts.map +1 -1
- package/dist/responses.d.ts +3 -3
- package/dist/responses.d.ts.map +1 -1
- package/dist/responses.js +67 -58
- package/dist/responses.js.map +1 -1
- package/dist/server.cjs +57 -30
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +58 -31
- package/dist/server.js.map +1 -1
- package/dist/stream-collapse.cjs.map +1 -1
- package/dist/stream-collapse.d.cts.map +1 -1
- package/dist/stream-collapse.d.ts.map +1 -1
- package/dist/stream-collapse.js.map +1 -1
- package/dist/types.d.cts +64 -11
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +64 -11
- package/dist/types.d.ts.map +1 -1
- package/fixtures/example-multi-turn.json +1 -1
- package/fixtures/example-tool-call.json +1 -1
- package/fixtures/examples/adk/gemini-agent.json +47 -0
- package/fixtures/examples/crewai/multi-agent-crew.json +16 -0
- package/fixtures/examples/langchain/agent-loop.json +27 -0
- package/fixtures/examples/llamaindex/aimock-config.json +62 -0
- package/fixtures/examples/llamaindex/rag-pipeline.json +34 -0
- package/fixtures/examples/mastra/agent-workflow.json +32 -0
- package/fixtures/examples/pydanticai/structured-output.json +15 -0
- package/package.json +2 -1
- package/skills/write-fixtures/SKILL.md +148 -22
package/dist/fixture-loader.d.ts
CHANGED
|
@@ -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":"
|
|
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"}
|
package/dist/fixture-loader.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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: "
|
|
164
|
+
message: "content is empty string"
|
|
114
165
|
});
|
|
115
|
-
|
|
166
|
+
if (response.toolCalls.length === 0) results.push({
|
|
116
167
|
severity: "warning",
|
|
117
168
|
fixtureIndex: i,
|
|
118
|
-
message: "
|
|
169
|
+
message: "toolCalls array is empty — fixture will never produce tool calls"
|
|
119
170
|
});
|
|
120
|
-
|
|
121
|
-
|
|
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: `
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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,
|