@copilotkit/aimock 1.42.1 → 1.43.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 +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +50 -0
- package/README.md +102 -16
- package/dist/agui-mock.cjs +1 -1
- package/dist/agui-mock.cjs.map +1 -1
- package/dist/agui-mock.d.cts.map +1 -1
- package/dist/agui-mock.d.ts.map +1 -1
- package/dist/agui-mock.js +1 -1
- package/dist/agui-mock.js.map +1 -1
- package/dist/agui-recorder.cjs +33 -11
- package/dist/agui-recorder.cjs.map +1 -1
- package/dist/agui-recorder.d.cts +10 -1
- package/dist/agui-recorder.d.cts.map +1 -1
- package/dist/agui-recorder.d.ts +10 -1
- package/dist/agui-recorder.d.ts.map +1 -1
- package/dist/agui-recorder.js +33 -11
- package/dist/agui-recorder.js.map +1 -1
- package/dist/agui-types.d.cts +1 -0
- package/dist/agui-types.d.cts.map +1 -1
- package/dist/agui-types.d.ts +1 -0
- package/dist/agui-types.d.ts.map +1 -1
- package/dist/aimock-cli.cjs +38 -10
- package/dist/aimock-cli.cjs.map +1 -1
- package/dist/aimock-cli.d.cts +5 -0
- package/dist/aimock-cli.d.cts.map +1 -1
- package/dist/aimock-cli.d.ts +5 -0
- package/dist/aimock-cli.d.ts.map +1 -1
- package/dist/aimock-cli.js +38 -10
- package/dist/aimock-cli.js.map +1 -1
- package/dist/batches.cjs +458 -0
- package/dist/batches.cjs.map +1 -0
- package/dist/batches.d.cts +57 -0
- package/dist/batches.d.cts.map +1 -0
- package/dist/batches.d.ts +57 -0
- package/dist/batches.d.ts.map +1 -0
- package/dist/batches.js +453 -0
- package/dist/batches.js.map +1 -0
- package/dist/bedrock-converse.cjs +12 -8
- package/dist/bedrock-converse.cjs.map +1 -1
- package/dist/bedrock-converse.d.cts.map +1 -1
- package/dist/bedrock-converse.d.ts.map +1 -1
- package/dist/bedrock-converse.js +14 -10
- package/dist/bedrock-converse.js.map +1 -1
- package/dist/bedrock.cjs +10 -8
- package/dist/bedrock.cjs.map +1 -1
- package/dist/bedrock.d.cts.map +1 -1
- package/dist/bedrock.d.ts.map +1 -1
- package/dist/bedrock.js +12 -10
- package/dist/bedrock.js.map +1 -1
- package/dist/byteplus-video.cjs +3 -3
- package/dist/byteplus-video.cjs.map +1 -1
- package/dist/byteplus-video.js +4 -4
- package/dist/byteplus-video.js.map +1 -1
- package/dist/chaos.cjs +622 -72
- package/dist/chaos.cjs.map +1 -1
- package/dist/chaos.d.cts +164 -7
- package/dist/chaos.d.cts.map +1 -1
- package/dist/chaos.d.ts +164 -7
- package/dist/chaos.d.ts.map +1 -1
- package/dist/chaos.js +613 -74
- package/dist/chaos.js.map +1 -1
- package/dist/cli.cjs +42 -23
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +42 -23
- package/dist/cli.js.map +1 -1
- package/dist/cohere.cjs +10 -8
- package/dist/cohere.cjs.map +1 -1
- package/dist/cohere.d.cts.map +1 -1
- package/dist/cohere.d.ts.map +1 -1
- package/dist/cohere.js +12 -10
- package/dist/cohere.js.map +1 -1
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/convert.cjs +1 -1
- package/dist/convert.cjs.map +1 -1
- package/dist/convert.js +1 -1
- package/dist/convert.js.map +1 -1
- package/dist/elevenlabs-audio.cjs +3 -3
- package/dist/elevenlabs-audio.cjs.map +1 -1
- package/dist/elevenlabs-audio.js +4 -4
- package/dist/elevenlabs-audio.js.map +1 -1
- package/dist/elevenlabs-voice.cjs +1053 -0
- package/dist/elevenlabs-voice.cjs.map +1 -0
- package/dist/elevenlabs-voice.d.cts +72 -0
- package/dist/elevenlabs-voice.d.cts.map +1 -0
- package/dist/elevenlabs-voice.d.ts +72 -0
- package/dist/elevenlabs-voice.d.ts.map +1 -0
- package/dist/elevenlabs-voice.js +1048 -0
- package/dist/elevenlabs-voice.js.map +1 -0
- package/dist/embeddings.cjs +8 -4
- package/dist/embeddings.cjs.map +1 -1
- package/dist/embeddings.d.cts.map +1 -1
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/embeddings.js +9 -5
- package/dist/embeddings.js.map +1 -1
- package/dist/fal-audio.cjs +3 -3
- package/dist/fal-audio.cjs.map +1 -1
- package/dist/fal-audio.js +4 -4
- package/dist/fal-audio.js.map +1 -1
- package/dist/fal.cjs +28 -4
- package/dist/fal.cjs.map +1 -1
- package/dist/fal.d.cts +16 -1
- package/dist/fal.d.cts.map +1 -1
- package/dist/fal.d.ts +16 -1
- package/dist/fal.d.ts.map +1 -1
- package/dist/fal.js +28 -5
- package/dist/fal.js.map +1 -1
- package/dist/files.cjs +1071 -0
- package/dist/files.cjs.map +1 -0
- package/dist/files.d.cts +68 -0
- package/dist/files.d.cts.map +1 -0
- package/dist/files.d.ts +68 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +1059 -0
- package/dist/files.js.map +1 -0
- package/dist/fine-tuning.cjs +1051 -0
- package/dist/fine-tuning.cjs.map +1 -0
- package/dist/fine-tuning.d.cts +96 -0
- package/dist/fine-tuning.d.cts.map +1 -0
- package/dist/fine-tuning.d.ts +96 -0
- package/dist/fine-tuning.d.ts.map +1 -0
- package/dist/fine-tuning.js +1044 -0
- package/dist/fine-tuning.js.map +1 -0
- package/dist/fixture-loader.cjs +0 -0
- package/dist/fixture-loader.cjs.map +1 -1
- package/dist/fixture-loader.d.cts +37 -5
- package/dist/fixture-loader.d.cts.map +1 -1
- package/dist/fixture-loader.d.ts +37 -5
- package/dist/fixture-loader.d.ts.map +1 -1
- package/dist/fixture-loader.js +0 -0
- package/dist/fixture-loader.js.map +1 -1
- package/dist/gemini-embeddings.cjs +2 -2
- package/dist/gemini-embeddings.cjs.map +1 -1
- package/dist/gemini-embeddings.js +3 -3
- package/dist/gemini-embeddings.js.map +1 -1
- package/dist/gemini-interactions.cjs +13 -7
- package/dist/gemini-interactions.cjs.map +1 -1
- package/dist/gemini-interactions.d.cts.map +1 -1
- package/dist/gemini-interactions.d.ts.map +1 -1
- package/dist/gemini-interactions.js +15 -9
- package/dist/gemini-interactions.js.map +1 -1
- package/dist/gemini.cjs +11 -7
- 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 +13 -9
- package/dist/gemini.js.map +1 -1
- package/dist/grok-video.cjs +14 -14
- package/dist/grok-video.cjs.map +1 -1
- package/dist/grok-video.js +15 -15
- package/dist/grok-video.js.map +1 -1
- package/dist/helpers.cjs +221 -5
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts +38 -2
- package/dist/helpers.d.cts.map +1 -1
- package/dist/helpers.d.ts +38 -2
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +213 -6
- package/dist/helpers.js.map +1 -1
- package/dist/images.cjs +54 -123
- package/dist/images.cjs.map +1 -1
- package/dist/images.d.cts.map +1 -1
- package/dist/images.d.ts.map +1 -1
- package/dist/images.js +55 -124
- package/dist/images.js.map +1 -1
- package/dist/index.cjs +41 -8
- package/dist/index.d.cts +12 -7
- package/dist/index.d.ts +12 -7
- package/dist/index.js +14 -10
- package/dist/jest.cjs +14 -7
- package/dist/jest.cjs.map +1 -1
- package/dist/jest.d.cts +3 -2
- package/dist/jest.d.cts.map +1 -1
- package/dist/jest.d.ts +3 -2
- package/dist/jest.d.ts.map +1 -1
- package/dist/jest.js +14 -7
- package/dist/jest.js.map +1 -1
- package/dist/journal.cjs +41 -4
- package/dist/journal.cjs.map +1 -1
- package/dist/journal.d.cts +44 -3
- package/dist/journal.d.cts.map +1 -1
- package/dist/journal.d.ts +44 -3
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +41 -5
- package/dist/journal.js.map +1 -1
- package/dist/live-fixture.cjs +1192 -0
- package/dist/live-fixture.cjs.map +1 -0
- package/dist/live-fixture.js +1187 -0
- package/dist/live-fixture.js.map +1 -0
- package/dist/live-recorder.cjs +256 -0
- package/dist/live-recorder.cjs.map +1 -0
- package/dist/live-recorder.js +254 -0
- package/dist/live-recorder.js.map +1 -0
- package/dist/live-replay.cjs +286 -0
- package/dist/live-replay.cjs.map +1 -0
- package/dist/live-replay.js +285 -0
- package/dist/live-replay.js.map +1 -0
- package/dist/live-sanitize.cjs +196 -0
- package/dist/live-sanitize.cjs.map +1 -0
- package/dist/live-sanitize.js +196 -0
- package/dist/live-sanitize.js.map +1 -0
- package/dist/live-types.d.cts +71 -0
- package/dist/live-types.d.cts.map +1 -0
- package/dist/live-types.d.ts +71 -0
- package/dist/live-types.d.ts.map +1 -0
- package/dist/llmock.cjs +52 -41
- package/dist/llmock.cjs.map +1 -1
- package/dist/llmock.d.cts +16 -5
- package/dist/llmock.d.cts.map +1 -1
- package/dist/llmock.d.ts +16 -5
- package/dist/llmock.d.ts.map +1 -1
- package/dist/llmock.js +53 -42
- package/dist/llmock.js.map +1 -1
- package/dist/messages.cjs +11 -7
- 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 +13 -9
- package/dist/messages.js.map +1 -1
- package/dist/metrics.cjs +258 -5
- package/dist/metrics.cjs.map +1 -1
- package/dist/metrics.d.cts +3 -1
- package/dist/metrics.d.cts.map +1 -1
- package/dist/metrics.d.ts +3 -1
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +255 -6
- package/dist/metrics.js.map +1 -1
- package/dist/moderation.cjs +10 -1
- package/dist/moderation.cjs.map +1 -1
- package/dist/moderation.d.cts.map +1 -1
- package/dist/moderation.d.ts.map +1 -1
- package/dist/moderation.js +10 -1
- package/dist/moderation.js.map +1 -1
- package/dist/ollama.cjs +5 -5
- package/dist/ollama.cjs.map +1 -1
- package/dist/ollama.js +6 -6
- package/dist/ollama.js.map +1 -1
- package/dist/openrouter-video.cjs +5 -5
- package/dist/openrouter-video.cjs.map +1 -1
- package/dist/openrouter-video.js +6 -6
- package/dist/openrouter-video.js.map +1 -1
- package/dist/recorder.cjs +45 -9
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.d.cts.map +1 -1
- package/dist/recorder.d.ts.map +1 -1
- package/dist/recorder.js +43 -11
- package/dist/recorder.js.map +1 -1
- package/dist/responses.cjs +4 -3
- package/dist/responses.cjs.map +1 -1
- package/dist/responses.js +5 -5
- package/dist/responses.js.map +1 -1
- package/dist/router.cjs +20 -33
- package/dist/router.cjs.map +1 -1
- package/dist/router.d.cts +4 -1
- package/dist/router.d.cts.map +1 -1
- package/dist/router.d.ts +4 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +20 -34
- package/dist/router.js.map +1 -1
- package/dist/server.cjs +701 -625
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +1 -0
- package/dist/server.d.cts.map +1 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +704 -628
- package/dist/server.js.map +1 -1
- package/dist/speech.cjs +2 -2
- package/dist/speech.cjs.map +1 -1
- package/dist/speech.js +3 -3
- package/dist/speech.js.map +1 -1
- package/dist/transcription.cjs +4 -4
- package/dist/transcription.cjs.map +1 -1
- package/dist/transcription.js +5 -5
- package/dist/transcription.js.map +1 -1
- package/dist/types.d.cts +119 -16
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +119 -16
- package/dist/types.d.ts.map +1 -1
- package/dist/validate-cli.cjs +925 -0
- package/dist/validate-cli.cjs.map +1 -0
- package/dist/validate-cli.js +924 -0
- package/dist/validate-cli.js.map +1 -0
- package/dist/veo-video.cjs +13 -13
- package/dist/veo-video.cjs.map +1 -1
- package/dist/veo-video.js +14 -14
- package/dist/veo-video.js.map +1 -1
- package/dist/video.cjs +4 -4
- package/dist/video.cjs.map +1 -1
- package/dist/video.d.cts +1 -1
- package/dist/video.d.cts.map +1 -1
- package/dist/video.d.ts +1 -1
- package/dist/video.d.ts.map +1 -1
- package/dist/video.js +5 -5
- package/dist/video.js.map +1 -1
- package/dist/vitest.cjs +13 -6
- package/dist/vitest.cjs.map +1 -1
- package/dist/vitest.d.cts +2 -1
- package/dist/vitest.d.cts.map +1 -1
- package/dist/vitest.d.ts +2 -1
- package/dist/vitest.d.ts.map +1 -1
- package/dist/vitest.js +14 -7
- package/dist/vitest.js.map +1 -1
- package/dist/watcher.cjs +2 -1
- package/dist/watcher.cjs.map +1 -1
- package/dist/watcher.js +2 -1
- package/dist/watcher.js.map +1 -1
- package/dist/ws-framing.cjs +150 -35
- package/dist/ws-framing.cjs.map +1 -1
- package/dist/ws-framing.d.cts +18 -3
- package/dist/ws-framing.d.cts.map +1 -1
- package/dist/ws-framing.d.ts +18 -3
- package/dist/ws-framing.d.ts.map +1 -1
- package/dist/ws-framing.js +151 -36
- package/dist/ws-framing.js.map +1 -1
- package/dist/ws-gemini-live.cjs +2 -2
- package/dist/ws-gemini-live.d.ts +2 -2
- package/dist/ws-gemini-live.js +2 -2
- package/dist/ws-live.cjs +401 -0
- package/dist/ws-live.cjs.map +1 -0
- package/dist/ws-live.js +400 -0
- package/dist/ws-live.js.map +1 -0
- package/dist/ws-realtime.cjs +46 -2
- package/dist/ws-realtime.cjs.map +1 -1
- package/dist/ws-realtime.d.cts.map +1 -1
- package/dist/ws-realtime.d.ts +2 -2
- package/dist/ws-realtime.d.ts.map +1 -1
- package/dist/ws-realtime.js +46 -2
- package/dist/ws-realtime.js.map +1 -1
- package/dist/ws-responses.cjs +2 -2
- package/dist/ws-responses.js +2 -2
- package/dist/ws-upstream.cjs +118 -0
- package/dist/ws-upstream.cjs.map +1 -0
- package/dist/ws-upstream.js +115 -0
- package/dist/ws-upstream.js.map +1 -0
- package/fixtures/openai-live-client.json +390 -0
- package/fixtures/openai-live-managed.json +709 -0
- package/package.json +1 -1
package/dist/fixture-loader.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { LiveOptions } from "./live-types.js";
|
|
1
2
|
import { Logger } from "./logger.js";
|
|
2
3
|
import { Fixture, FixtureFileResponse, FixtureResponse } from "./types.js";
|
|
3
4
|
|
|
@@ -10,14 +11,45 @@ import { Fixture, FixtureFileResponse, FixtureResponse } from "./types.js";
|
|
|
10
11
|
* `fallthrough` OpenRouter-failover flag) pass through unmodified via the
|
|
11
12
|
* shallow clone below.
|
|
12
13
|
*/
|
|
13
|
-
declare function normalizeResponse(raw: FixtureFileResponse): FixtureResponse;
|
|
14
|
-
declare function loadFixtureFile(filePath: string, logger?: Logger): Fixture[];
|
|
15
|
-
declare function loadFixturesFromDir(dirPath: string, logger?: Logger): Fixture[];
|
|
14
|
+
declare function normalizeResponse(raw: FixtureFileResponse, liveOptions?: LiveOptions): FixtureResponse;
|
|
15
|
+
declare function loadFixtureFile(filePath: string, logger?: Logger, liveOptions?: LiveOptions): Fixture[];
|
|
16
|
+
declare function loadFixturesFromDir(dirPath: string, logger?: Logger, liveOptions?: LiveOptions): Fixture[];
|
|
17
|
+
/**
|
|
18
|
+
* The fixture indices a finding's message NAMES, carried as data.
|
|
19
|
+
*
|
|
20
|
+
* Two rules (`duplicate userMessage` and the catch-all ordering check) name
|
|
21
|
+
* OTHER fixtures in the array they were given, so their message text embeds an
|
|
22
|
+
* index. A consumer that validates a different array than the one the reader
|
|
23
|
+
* sees — `aimock validate` runs the rules per file and then over the union of
|
|
24
|
+
* every file — has to renumber those indices, and the only safe way to do that
|
|
25
|
+
* is to re-render from the numbers rather than to rewrite the rendered prose:
|
|
26
|
+
* `duplicate userMessage` interpolates the fixture's OWN `userMessage`, so a
|
|
27
|
+
* regex over the message corrupts user-authored text that happens to read
|
|
28
|
+
* "fixture 3".
|
|
29
|
+
*/
|
|
30
|
+
type ValidationRef = {
|
|
31
|
+
rule: "duplicate-user-message";
|
|
32
|
+
userMessage: string;
|
|
33
|
+
shadows: number;
|
|
34
|
+
} | {
|
|
35
|
+
rule: "catch-all-not-last";
|
|
36
|
+
shadowsFrom: number;
|
|
37
|
+
shadowsThrough: number;
|
|
38
|
+
};
|
|
16
39
|
interface ValidationResult {
|
|
17
40
|
severity: "error" | "warning";
|
|
18
41
|
fixtureIndex: number;
|
|
19
42
|
message: string;
|
|
43
|
+
/** Present only on the rules whose message names other fixtures. */
|
|
44
|
+
ref?: ValidationRef;
|
|
20
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Render a ref-carrying finding's message with each named index passed through
|
|
48
|
+
* `renderIndex`. With `String` it reproduces the message `validateFixtures`
|
|
49
|
+
* itself emits; `aimock validate` passes a renderer that resolves a union
|
|
50
|
+
* index to `<file> #<index-in-that-file>`.
|
|
51
|
+
*/
|
|
52
|
+
declare function renderValidationRef(ref: ValidationRef, renderIndex: (fixtureIndex: number) => string): string;
|
|
21
53
|
/**
|
|
22
54
|
* Human-readable form of the status range an injected error may carry.
|
|
23
55
|
* Measured on Node v26: `res.writeHead` throws outside 100-999 (the injected
|
|
@@ -27,8 +59,8 @@ interface ValidationResult {
|
|
|
27
59
|
* client sees it — so a mock server allows it.
|
|
28
60
|
*/
|
|
29
61
|
|
|
30
|
-
declare function validateFixtures(fixtures: Fixture[]): ValidationResult[];
|
|
62
|
+
declare function validateFixtures(fixtures: Fixture[], liveOptions?: LiveOptions): ValidationResult[];
|
|
31
63
|
//# sourceMappingURL=fixture-loader.d.ts.map
|
|
32
64
|
//#endregion
|
|
33
|
-
export { ValidationResult, loadFixtureFile, loadFixturesFromDir, normalizeResponse, validateFixtures };
|
|
65
|
+
export { ValidationRef, ValidationResult, loadFixtureFile, loadFixturesFromDir, normalizeResponse, renderValidationRef, validateFixtures };
|
|
34
66
|
//# 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":";;;;;;;;AAoCA;;;;;AAGkB,iBAHF,iBAAA,CAGE,GAAA,EAFX,mBAEW,EAAA,WAAA,CAAA,EADF,WACE,CAAA,EAAf,eAAe;AAkIa,iBAAf,eAAA,CAAe,QAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EAEpB,MAFoB,EAAA,WAAA,CAAA,EAGf,WAHe,CAAA,EAI5B,OAJ4B,EAAA;AAEpB,iBA+BK,mBAAA,CA/BL,OAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EAiCA,MAjCA,EAAA,WAAA,CAAA,EAkCK,WAlCL,CAAA,EAmCR,OAnCQ,EAAA;;;;AA+BX;;;;;;AAoEA;AAIA;AAcA;AAkZA;AAAgC,KApapB,aAAA,GAoaoB;MACpB,EAAA,wBAAA;aACI,EAAA,MAAA;SACb,EAAA,MAAA;CAAgB,GAAA;;;;;UAnaF,gBAAA;;;;;QAKT;;;;;;;;iBASQ,mBAAA,MACT;;;;;;;;;;iBAiZS,gBAAA,WACJ,yBACI,cACb"}
|
package/dist/fixture-loader.js
CHANGED
|
Binary file
|
|
@@ -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 {\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 isJSONResponse,\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, and the ErrorResponse\n * `fallthrough` OpenRouter-failover flag) pass through unmodified via the\n * shallow clone below.\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 // Carry the optional ordered `blocks` array through, mirroring the\n // toolCalls[].arguments idiom above: auto-stringify object `arguments` on\n // each `toolCall` block. Gated on Array.isArray so a malformed (non-array)\n // `blocks` value passes through untouched rather than crashing — downstream\n // validation/builders own shape rejection. Absent `blocks` → key absent.\n if (Array.isArray(response.blocks)) {\n response.blocks = (response.blocks as Array<Record<string, unknown>>).map((block) => {\n if (\n block != null &&\n block.type === \"toolCall\" &&\n typeof block.arguments === \"object\" &&\n block.arguments !== null\n ) {\n return { ...block, arguments: JSON.stringify(block.arguments) };\n }\n return block;\n });\n }\n\n return response as unknown as FixtureResponse;\n}\n\nexport function entryToFixture(entry: FixtureFileEntry, logger?: Logger): Fixture {\n const fixture: Fixture = {\n match: {\n userMessage: entry.match.userMessage,\n systemMessage: entry.match.systemMessage,\n inputText: entry.match.inputText,\n toolCallId: entry.match.toolCallId,\n toolResultContains: entry.match.toolResultContains,\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 ...(entry.match.turnIndex !== undefined && {\n turnIndex: entry.match.turnIndex,\n }),\n ...(entry.match.hasToolResult !== undefined && {\n hasToolResult: entry.match.hasToolResult,\n }),\n ...(entry.match.context !== undefined && { context: entry.match.context }),\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.recordedTimings !== undefined && { recordedTimings: entry.recordedTimings }),\n ...(entry.replaySpeed != null && { replaySpeed: entry.replaySpeed }),\n ...(entry.chaos !== undefined && { chaos: entry.chaos }),\n ...(entry.openRouterProcessing !== undefined && {\n openRouterProcessing: entry.openRouterProcessing,\n }),\n ...(entry.metadata !== undefined && { metadata: entry.metadata }),\n };\n\n // Sanitize recordedTimings to guard against NaN or negative values that\n // would silently degrade replay timing calculations.\n if (fixture.recordedTimings) {\n const rt = fixture.recordedTimings;\n if (!Number.isFinite(rt.ttftMs) || rt.ttftMs < 0) rt.ttftMs = 0;\n rt.interChunkDelaysMs = Array.isArray(rt.interChunkDelaysMs)\n ? rt.interChunkDelaysMs.filter((d) => Number.isFinite(d) && d >= 0)\n : [];\n if (!Number.isFinite(rt.totalDurationMs) || rt.totalDurationMs < 0) rt.totalDurationMs = 0;\n }\n\n if (fixture.replaySpeed != null && fixture.replaySpeed <= 0) {\n logger?.warn(`Fixture replaySpeed must be positive, got ${fixture.replaySpeed}. Ignoring.`);\n delete fixture.replaySpeed;\n }\n\n return fixture;\n}\n\n/**\n * Serialise a match value (string | string[] | RegExp | undefined) into a\n * dedup-key fragment that is kind-aware, so a string, a RegExp with the same\n * source, and an array containing that string do not collide. A plain template\n * literal would coerce `/foo/` and `\"foo\"` to the same text and join `[\"foo\"]`\n * to `\"foo\"`, wrongly deduping fixtures that the router would treat as distinct.\n * Used by the duplicate-userMessage dedup key in validateFixtures.\n */\nfunction serializeMatcher(value: string | string[] | RegExp | undefined): string {\n if (value === undefined) return \"\";\n if (value instanceof RegExp) return `re:${value.source}\u0000${value.flags}`;\n if (Array.isArray(value)) return `arr:${JSON.stringify(value)}`;\n return `str:${value}`;\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((e) => entryToFixture(e, logger));\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 const subdirs: string[] = [];\n for (const name of entries) {\n const fullPath = join(dirPath, name);\n try {\n if (statSync(fullPath).isDirectory()) {\n subdirs.push(name);\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 // Recurse into all subdirectories (full depth) to support nested layouts\n // like showcase/aimock/d6/<integration>/<feature>.json.\n subdirs.sort();\n for (const sub of subdirs) {\n fixtures.push(...loadFixturesFromDir(join(dirPath, sub), 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\nfunction validateBlocks(\n response: { blocks?: unknown; content?: unknown; toolCalls?: unknown },\n fixtureIndex: number,\n results: ValidationResult[],\n): void {\n if (response.blocks === undefined) return;\n\n // Mirrors the toolCalls checks: reject malformed `blocks` at LOAD time so a\n // bad blocks array never reaches the dispatch/builder (where\n // resolveFixtureBlocks throws AFTER the journal has already recorded\n // status:200, yielding a journal-200/client-500 mismatch). #274 F3+F8.\n if (!Array.isArray(response.blocks)) {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: `blocks must be an array, got ${typeof response.blocks}`,\n });\n return;\n }\n\n for (let j = 0; j < response.blocks.length; j++) {\n const block = response.blocks[j] as Record<string, unknown> | null | undefined;\n if (typeof block !== \"object\" || block === null) {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: `blocks[${j}] must be an object`,\n });\n continue;\n }\n if (block.type !== \"text\" && block.type !== \"toolCall\") {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: `blocks[${j}].type must be \"text\" or \"toolCall\", got ${JSON.stringify(block.type)}`,\n });\n continue;\n }\n if (block.type === \"text\") {\n if (typeof block.text !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: `blocks[${j}].text must be a string, got ${typeof block.text}`,\n });\n } else if (block.text === \"\") {\n // Mirror the content/toolCalls \"empty string\" rejection: an empty-text\n // block produces a meaningless/spurious wire chunk on replay.\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: `blocks[${j}].text is empty string`,\n });\n }\n } else {\n // toolCall block — mirror toolCalls[] name + arguments checks.\n if (typeof block.name !== \"string\" || block.name === \"\") {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: `blocks[${j}].name must be a non-empty string`,\n });\n }\n // `arguments` is JSON-string in runtime form (normalizeResponse already\n // stringified object/array args); accept a valid-JSON string or an object.\n if (typeof block.arguments === \"string\") {\n try {\n JSON.parse(block.arguments);\n } catch {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: `blocks[${j}].arguments is not valid JSON: ${block.arguments}`,\n });\n }\n } else if (typeof block.arguments !== \"object\" || block.arguments === null) {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: `blocks[${j}].arguments must be a JSON string or object, got ${typeof block.arguments}`,\n });\n }\n if (block.id !== undefined && typeof block.id !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: `blocks[${j}].id must be a string, got ${typeof block.id}`,\n });\n }\n }\n }\n\n // blocks-vs-content/toolCalls divergence (#274 P2). When a fixture carries\n // BOTH `blocks` AND legacy `content`/`toolCalls` (allowed but unusual now\n // that blocks-only is first-class), builders stream `blocks` and IGNORE the\n // redundant `content`/`toolCalls`. If those disagree it is a silent footgun,\n // so WARN (not a hard error). Stay silent on the clean blocks-only path\n // (neither legacy field present) — that is the intended shape.\n const hasLegacyContent = typeof response.content === \"string\";\n const hasLegacyToolCalls = Array.isArray(response.toolCalls);\n if ((hasLegacyContent || hasLegacyToolCalls) && Array.isArray(response.blocks)) {\n const textBlocks = response.blocks.filter(\n (b): b is { type: \"text\"; text: string } =>\n b != null &&\n typeof b === \"object\" &&\n (b as { type?: unknown }).type === \"text\" &&\n typeof (b as { text?: unknown }).text === \"string\",\n );\n const toolCallBlockNames = response.blocks\n .filter(\n (b): b is { type: \"toolCall\"; name: string } =>\n b != null &&\n typeof b === \"object\" &&\n (b as { type?: unknown }).type === \"toolCall\" &&\n typeof (b as { name?: unknown }).name === \"string\",\n )\n .map((b) => b.name);\n\n // Text divergence: blocks' concatenated text vs legacy `content`.\n if (hasLegacyContent) {\n const blocksText = textBlocks.map((b) => b.text).join(\"\");\n if (blocksText !== response.content) {\n results.push({\n severity: \"warning\",\n fixtureIndex,\n message:\n \"blocks text diverges from content — builders stream blocks and ignore the redundant content field\",\n });\n }\n }\n\n // ToolCall divergence: blocks' ordered toolCall names vs legacy `toolCalls`.\n if (hasLegacyToolCalls) {\n const legacyNames = (response.toolCalls as Array<{ name?: unknown }>).map((tc) =>\n typeof tc?.name === \"string\" ? tc.name : undefined,\n );\n const sameNames =\n legacyNames.length === toolCallBlockNames.length &&\n legacyNames.every((n, k) => n === toolCallBlockNames[k]);\n if (!sameNames) {\n results.push({\n severity: \"warning\",\n fixtureIndex,\n message:\n \"blocks toolCalls diverge from toolCalls — builders stream blocks and ignore the redundant toolCalls field\",\n });\n }\n }\n }\n}\n\n/**\n * Human-readable form of the status range an injected error may carry.\n * Measured on Node v26: `res.writeHead` throws outside 100-999 (the injected\n * error is then lost and the caller sees a generic 500), and a 1xx status is\n * accepted but cannot terminate a response, so it hangs the request until the\n * client times out. 600-999 is unassigned, NOT invalid — Node sends it and a\n * client sees it — so a mock server allows it.\n */\nexport const INJECTED_STATUS_RANGE =\n \"an integer between 200 and 999 (1xx cannot terminate a response and would hang the client)\";\n\n/**\n * Shared by the three doors that queue an error whose `status` reaches\n * `res.writeHead`: a fixture's `response.status` (below), `POST\n * /__aimock/error`, and `LLMock.nextRequestError`.\n */\nexport function isInjectableStatus(status: unknown): status is number {\n // The typeof is for the COMPILER — it narrows `unknown` so the comparisons\n // below type-check. At runtime `Number.isInteger` already excludes every\n // non-number, so mutating it away fails nothing.\n if (typeof status !== \"number\") return false;\n return Number.isInteger(status) && status >= 200 && status <= 999;\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 // Skip response-shape validation for function responses — they are\n // evaluated at runtime so we cannot statically inspect them.\n if (typeof response === \"function\") {\n // Still validate match fields and numeric options below.\n } else {\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 !isJSONResponse(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, video, or json)\",\n });\n }\n\n // When a non-empty ordered `blocks` array is present, the builders stream\n // `blocks` and IGNORE the legacy `content` mirror (see validateBlocks's\n // divergence note + isContentWithToolCallsResponse's BLOCKS-ONLY clause).\n // So an empty-string `content` is harmless in that case and must NOT raise\n // the \"content is empty string\" hard error. Fixtures WITHOUT blocks keep\n // the error (an empty content with no blocks produces no output).\n const hasNonEmptyBlocks =\n Array.isArray((response as { blocks?: unknown }).blocks) &&\n (response as { blocks: unknown[] }).blocks.length > 0;\n\n // Text response checks\n if (isTextResponse(response)) {\n if (response.content === \"\" && !hasNonEmptyBlocks) {\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 // The guard now also matches a BLOCKS-ONLY fixture (non-empty `blocks`,\n // no `content`/`toolCalls`). For that shape the content/toolCalls checks\n // below don't apply (and `content`/`toolCalls` are undefined) — the\n // ordered `blocks` array is validated separately by `validateBlocks`\n // immediately after this block. So gate the legacy field checks on the\n // fields actually being present, mirroring the builders' branch-on-blocks.\n if (typeof response.content === \"string\") {\n if (response.content === \"\" && !hasNonEmptyBlocks) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"content is empty string\",\n });\n }\n }\n if (Array.isArray(response.toolCalls)) {\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 validateReasoning(response, i, results);\n validateWebSearches(response, i, results);\n }\n\n // Optional ordered `blocks` checks — validated whenever present on the\n // response, regardless of which content/toolCalls guard matched, so a\n // malformed blocks array is rejected at LOAD rather than at dispatch.\n validateBlocks(\n response as { blocks?: unknown; content?: unknown; toolCalls?: unknown },\n i,\n 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 validateWebSearches(response, i, results);\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 // Shared with `POST /__aimock/error` and `LLMock.nextRequestError`\n // (see `isInjectableStatus` above): a fixture's status lands on the same\n // `res.writeHead`, so it is accepted or rejected identically.\n if (response.status !== undefined && !isInjectableStatus(response.status)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `error status ${response.status} is not a valid HTTP status code — must be ${INJECTED_STATUS_RANGE}`,\n });\n }\n // `fallthrough` gates OpenRouter `models[]` failover and is read as\n // `!== false`, so a non-boolean (`\"false\"`, 0, null) would silently\n // fall through — the OPPOSITE of the intended terminal behavior. Reject\n // it at load so the fail-closed half cannot quietly fail open.\n if (response.fallthrough !== undefined && typeof response.fallthrough !== \"boolean\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `error.fallthrough must be a boolean, got ${typeof response.fallthrough}`,\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 // Audio response checks — validate object-form audio\n if (isAudioResponse(response) && typeof response.audio === \"object\") {\n const audioObj = response.audio;\n if (typeof audioObj.b64Json !== \"string\" || audioObj.b64Json === \"\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"audio.b64Json must be a non-empty string\",\n });\n }\n if (audioObj.contentType !== undefined && typeof audioObj.contentType !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `audio.contentType must be a string, got ${typeof audioObj.contentType}`,\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 // Numeric token fields must be numbers, but the OpenRouter cost\n // sub-fields carry non-numeric shapes (see ResponseOverrides.usage\n // in types.ts): cost_details / prompt_tokens_details /\n // completion_tokens_details are objects and is_byok is a boolean.\n // Validate each per its documented type so cost-scripting fixtures\n // are not rejected with a misleading \"must be a number\" error.\n // Known numeric inner scalars per object field (see\n // OpenRouterUsageExtras in types.ts). Validating only these keeps\n // the index-signature ([key: string]: unknown) escape hatch open\n // for forward-compat fields while still catching malformed values\n // on the documented ones — otherwise a bad inner scalar (e.g.\n // cost_details.upstream_inference_cost: \"abc\") passes load-time\n // validation and only blows up at replay in the canonical SDK.\n const numericInnerFields: Record<string, readonly string[]> = {\n cost_details: [\n \"upstream_inference_cost\",\n \"upstream_inference_prompt_cost\",\n \"upstream_inference_completions_cost\",\n ],\n prompt_tokens_details: [\"cached_tokens\", \"cache_write_tokens\", \"audio_tokens\"],\n completion_tokens_details: [\"reasoning_tokens\"],\n };\n const objectUsageFields = new Set(Object.keys(numericInnerFields));\n for (const key of Object.keys(r.usage)) {\n const val = (r.usage as Record<string, unknown>)[key];\n if (val === undefined) continue;\n if (objectUsageFields.has(key)) {\n if (typeof val !== \"object\" || val === null || Array.isArray(val)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"usage.${key}\" must be an object, got ${\n Array.isArray(val) ? \"array\" : typeof val\n }`,\n });\n } else {\n // Type-check the documented inner scalars; each is optional\n // (required-if-present), so only validate keys that are set.\n const inner = val as Record<string, unknown>;\n for (const innerKey of numericInnerFields[key]) {\n const innerVal = inner[innerKey];\n if (innerVal !== undefined && typeof innerVal !== \"number\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"usage.${key}.${innerKey}\" must be a number, got ${typeof innerVal}`,\n });\n }\n }\n }\n } else if (key === \"is_byok\") {\n if (typeof val !== \"boolean\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"usage.is_byok\" must be a boolean, got ${typeof val}`,\n });\n }\n } else if (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 } // end: skip response-shape validation for function responses\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 // Match field type checks\n if (f.match.turnIndex !== undefined) {\n if (\n typeof f.match.turnIndex !== \"number\" ||\n f.match.turnIndex < 0 ||\n !Number.isInteger(f.match.turnIndex)\n ) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"match.turnIndex must be a non-negative integer\",\n });\n }\n }\n if (f.match.sequenceIndex !== undefined) {\n if (\n typeof f.match.sequenceIndex !== \"number\" ||\n f.match.sequenceIndex < 0 ||\n !Number.isInteger(f.match.sequenceIndex)\n ) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"match.sequenceIndex must be a non-negative integer\",\n });\n }\n }\n if (f.match.hasToolResult !== undefined && typeof f.match.hasToolResult !== \"boolean\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `match.hasToolResult must be a boolean, got ${typeof f.match.hasToolResult}`,\n });\n }\n if (f.match.toolResultContains !== undefined) {\n if (typeof f.match.toolResultContains !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `match.toolResultContains must be a string, got ${typeof f.match.toolResultContains}`,\n });\n } else if (f.match.toolResultContains.length === 0) {\n // An empty substring is always contained, so the gate would silently\n // act as \"last message is any tool result\" — reject it as an authoring\n // mistake rather than let it shadow later fixtures.\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"match.toolResultContains must be a non-empty string\",\n });\n }\n }\n if (f.match.systemMessage !== undefined) {\n const sm = f.match.systemMessage;\n if (typeof sm === \"string\") {\n // ok\n } else if (Array.isArray(sm)) {\n if (sm.length === 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `match.systemMessage array must contain at least one substring`,\n });\n } else {\n for (let j = 0; j < sm.length; j++) {\n if (typeof sm[j] !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `match.systemMessage[${j}] must be a string, got ${typeof sm[j]}`,\n });\n }\n }\n }\n } else {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `match.systemMessage must be a string or string[], got ${typeof sm}`,\n });\n }\n }\n if (f.match.context !== undefined && typeof f.match.context !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `match.context must be a string, got ${typeof f.match.context}`,\n });\n }\n\n // --- Warning checks ---\n\n // Duplicate userMessage shadowing — two fixtures are only genuine\n // duplicates when they would match the SAME requests, so the dedup key must\n // include EVERY match discriminator the router (matchFixtureDiagnostic in\n // router.ts) actually gates on: userMessage, systemMessage, inputText,\n // toolCallId, toolResultContains, toolName, model, responseFormat, endpoint,\n // context, sequenceIndex, turnIndex, and hasToolResult. Omitting any of these\n // (the old key only carried turnIndex/hasToolResult/toolResultContains/\n // sequenceIndex/context) flags two legitimately-distinct fixtures — e.g. two\n // that differ ONLY in toolCallId or model — as false duplicates.\n //\n // `predicate` is a function and cannot be compared for equivalence, so it is\n // never folded into a shared key: a fixture carrying a predicate is treated\n // as unconditionally distinct (keyed by its own index) so it neither shadows\n // nor is shadowed by another fixture on the userMessage axis.\n //\n // Values are serialised kind-aware so RegExp / string[] matchers do not\n // collide (a template literal would coerce a RegExp to its source and an\n // array via join, losing the distinction) — mirroring describeMatch in\n // router.ts.\n const um = f.match.userMessage;\n if (typeof um === \"string\" && um) {\n const m = f.match;\n const dedupKey =\n m.predicate !== undefined\n ? `predicate:${i}`\n : [\n serializeMatcher(m.userMessage),\n serializeMatcher(m.systemMessage),\n serializeMatcher(m.inputText),\n m.toolCallId,\n m.toolResultContains,\n m.toolName,\n serializeMatcher(m.model),\n m.responseFormat,\n m.endpoint,\n m.context,\n m.sequenceIndex,\n m.turnIndex,\n m.hasToolResult,\n ]\n .map((v) => (v === undefined ? \"\" : String(v)))\n .join(\"|\");\n const prev = seenUserMessages.get(dedupKey);\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(dedupKey, 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.context !== undefined ||\n match.userMessage !== undefined ||\n match.systemMessage !== undefined ||\n match.inputText !== undefined ||\n match.responseFormat !== undefined ||\n match.toolCallId !== undefined ||\n match.toolResultContains !== undefined ||\n match.toolName !== undefined ||\n match.model !== undefined ||\n match.predicate !== undefined ||\n match.turnIndex !== undefined ||\n match.sequenceIndex !== undefined ||\n match.hasToolResult !== 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 type { LiveOptions } from \"./live-types.js\";\nimport { normalizeLiveFixture } from \"./live-fixture.js\";\nimport { readFileSync, readdirSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type {\n ChatCompletionRequest,\n Fixture,\n FixtureFile,\n FixtureFileEntry,\n FixtureFileResponse,\n FixtureResponse,\n ResponseOverrides,\n} from \"./types.js\";\nimport {\n isLiveResponse,\n isTextResponse,\n isToolCallResponse,\n isContentWithToolCallsResponse,\n isErrorResponse,\n isEmbeddingResponse,\n isImageResponse,\n isAudioResponse,\n isTranscriptionResponse,\n isVideoResponse,\n isJSONResponse,\n} from \"./helpers.js\";\nimport type { Logger } from \"./logger.js\";\nimport { CHAOS_FIELDS, CHAOS_FIELD_NAMES, parseChaosField } from \"./chaos.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, and the ErrorResponse\n * `fallthrough` OpenRouter-failover flag) pass through unmodified via the\n * shallow clone below.\n */\nexport function normalizeResponse(\n raw: FixtureFileResponse,\n liveOptions?: LiveOptions,\n): FixtureResponse {\n if (isLiveResponse(raw)) return normalizeLiveFixture(raw, liveOptions);\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 // Carry the optional ordered `blocks` array through, mirroring the\n // toolCalls[].arguments idiom above: auto-stringify object `arguments` on\n // each `toolCall` block. Gated on Array.isArray so a malformed (non-array)\n // `blocks` value passes through untouched rather than crashing — downstream\n // validation/builders own shape rejection. Absent `blocks` → key absent.\n if (Array.isArray(response.blocks)) {\n response.blocks = (response.blocks as Array<Record<string, unknown>>).map((block) => {\n if (\n block != null &&\n block.type === \"toolCall\" &&\n typeof block.arguments === \"object\" &&\n block.arguments !== null\n ) {\n return { ...block, arguments: JSON.stringify(block.arguments) };\n }\n return block;\n });\n }\n\n return response as unknown as FixtureResponse;\n}\n\nexport function entryToFixture(\n entry: FixtureFileEntry,\n logger?: Logger,\n liveOptions?: LiveOptions,\n): Fixture {\n const fixture: Fixture = {\n match: {\n userMessage: entry.match.userMessage,\n systemMessage: entry.match.systemMessage,\n inputText: entry.match.inputText,\n toolCallId: entry.match.toolCallId,\n toolResultContains: entry.match.toolResultContains,\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 ...(entry.match.turnIndex !== undefined && {\n turnIndex: entry.match.turnIndex,\n }),\n ...(entry.match.hasToolResult !== undefined && {\n hasToolResult: entry.match.hasToolResult,\n }),\n ...(entry.match.context !== undefined && { context: entry.match.context }),\n },\n response:\n entry.match.endpoint === \"openai-live\"\n ? normalizeLiveFixture(entry.response, liveOptions)\n : normalizeResponse(entry.response, liveOptions),\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.recordedTimings !== undefined && { recordedTimings: entry.recordedTimings }),\n ...(entry.replaySpeed != null && { replaySpeed: entry.replaySpeed }),\n ...(entry.chaos !== undefined && { chaos: entry.chaos }),\n ...(entry.openRouterProcessing !== undefined && {\n openRouterProcessing: entry.openRouterProcessing,\n }),\n ...(entry.metadata !== undefined && { metadata: entry.metadata }),\n };\n\n // Sanitize recordedTimings to guard against NaN or negative values that\n // would silently degrade replay timing calculations.\n if (fixture.recordedTimings) {\n const rt = fixture.recordedTimings;\n if (!Number.isFinite(rt.ttftMs) || rt.ttftMs < 0) rt.ttftMs = 0;\n rt.interChunkDelaysMs = Array.isArray(rt.interChunkDelaysMs)\n ? rt.interChunkDelaysMs.filter((d) => Number.isFinite(d) && d >= 0)\n : [];\n if (!Number.isFinite(rt.totalDurationMs) || rt.totalDurationMs < 0) rt.totalDurationMs = 0;\n }\n\n if (fixture.replaySpeed != null && fixture.replaySpeed <= 0) {\n logger?.warn(`Fixture replaySpeed must be positive, got ${fixture.replaySpeed}. Ignoring.`);\n delete fixture.replaySpeed;\n }\n\n return fixture;\n}\n\n/**\n * Serialise a match value (string | string[] | RegExp | undefined) into a\n * dedup-key fragment that is kind-aware, so a string, a RegExp with the same\n * source, and an array containing that string do not collide. A plain template\n * literal would coerce `/foo/` and `\"foo\"` to the same text and join `[\"foo\"]`\n * to `\"foo\"`, wrongly deduping fixtures that the router would treat as distinct.\n * Used by the duplicate-userMessage dedup key in validateFixtures.\n */\nfunction serializeMatcher(value: string | string[] | RegExp | undefined): string {\n if (value === undefined) return \"\";\n if (value instanceof RegExp) return `re:${value.source}\\u0000${value.flags}`;\n if (Array.isArray(value)) return `arr:${JSON.stringify(value)}`;\n return `str:${value}`;\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(\n filePath: string,\n logger?: Logger,\n liveOptions?: LiveOptions,\n): 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((e) => entryToFixture(e, logger, liveOptions));\n}\n\nexport function loadFixturesFromDir(\n dirPath: string,\n logger?: Logger,\n liveOptions?: LiveOptions,\n): 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 const subdirs: string[] = [];\n for (const name of entries) {\n const fullPath = join(dirPath, name);\n try {\n if (statSync(fullPath).isDirectory()) {\n subdirs.push(name);\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, liveOptions));\n }\n\n // Recurse into all subdirectories (full depth) to support nested layouts\n // like showcase/aimock/d6/<integration>/<feature>.json.\n subdirs.sort();\n for (const sub of subdirs) {\n fixtures.push(...loadFixturesFromDir(join(dirPath, sub), logger, liveOptions));\n }\n\n return fixtures;\n}\n\n// ---------------------------------------------------------------------------\n// Fixture validation\n// ---------------------------------------------------------------------------\n\n/**\n * The fixture indices a finding's message NAMES, carried as data.\n *\n * Two rules (`duplicate userMessage` and the catch-all ordering check) name\n * OTHER fixtures in the array they were given, so their message text embeds an\n * index. A consumer that validates a different array than the one the reader\n * sees — `aimock validate` runs the rules per file and then over the union of\n * every file — has to renumber those indices, and the only safe way to do that\n * is to re-render from the numbers rather than to rewrite the rendered prose:\n * `duplicate userMessage` interpolates the fixture's OWN `userMessage`, so a\n * regex over the message corrupts user-authored text that happens to read\n * \"fixture 3\".\n */\nexport type ValidationRef =\n | { rule: \"duplicate-user-message\"; userMessage: string; shadows: number }\n | { rule: \"catch-all-not-last\"; shadowsFrom: number; shadowsThrough: number };\n\nexport interface ValidationResult {\n severity: \"error\" | \"warning\";\n fixtureIndex: number;\n message: string;\n /** Present only on the rules whose message names other fixtures. */\n ref?: ValidationRef;\n}\n\n/**\n * Render a ref-carrying finding's message with each named index passed through\n * `renderIndex`. With `String` it reproduces the message `validateFixtures`\n * itself emits; `aimock validate` passes a renderer that resolves a union\n * index to `<file> #<index-in-that-file>`.\n */\nexport function renderValidationRef(\n ref: ValidationRef,\n renderIndex: (fixtureIndex: number) => string,\n): string {\n return ref.rule === \"duplicate-user-message\"\n ? `duplicate userMessage '${ref.userMessage}' — shadows fixture ${renderIndex(ref.shadows)}`\n : `empty match acts as catch-all but is not the last fixture — shadows fixtures ${renderIndex(\n ref.shadowsFrom,\n )}+`;\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\nfunction validateBlocks(\n response: { blocks?: unknown; content?: unknown; toolCalls?: unknown },\n fixtureIndex: number,\n results: ValidationResult[],\n): void {\n if (response.blocks === undefined) return;\n\n // Mirrors the toolCalls checks: reject malformed `blocks` at LOAD time so a\n // bad blocks array never reaches the dispatch/builder (where\n // resolveFixtureBlocks throws AFTER the journal has already recorded\n // status:200, yielding a journal-200/client-500 mismatch). #274 F3+F8.\n if (!Array.isArray(response.blocks)) {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: `blocks must be an array, got ${typeof response.blocks}`,\n });\n return;\n }\n\n for (let j = 0; j < response.blocks.length; j++) {\n const block = response.blocks[j] as Record<string, unknown> | null | undefined;\n if (typeof block !== \"object\" || block === null) {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: `blocks[${j}] must be an object`,\n });\n continue;\n }\n if (block.type !== \"text\" && block.type !== \"toolCall\") {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: `blocks[${j}].type must be \"text\" or \"toolCall\", got ${JSON.stringify(block.type)}`,\n });\n continue;\n }\n if (block.type === \"text\") {\n if (typeof block.text !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: `blocks[${j}].text must be a string, got ${typeof block.text}`,\n });\n } else if (block.text === \"\") {\n // Mirror the content/toolCalls \"empty string\" rejection: an empty-text\n // block produces a meaningless/spurious wire chunk on replay.\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: `blocks[${j}].text is empty string`,\n });\n }\n } else {\n // toolCall block — mirror toolCalls[] name + arguments checks.\n if (typeof block.name !== \"string\" || block.name === \"\") {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: `blocks[${j}].name must be a non-empty string`,\n });\n }\n // `arguments` is JSON-string in runtime form (normalizeResponse already\n // stringified object/array args); accept a valid-JSON string or an object.\n if (typeof block.arguments === \"string\") {\n try {\n JSON.parse(block.arguments);\n } catch {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: `blocks[${j}].arguments is not valid JSON: ${block.arguments}`,\n });\n }\n } else if (typeof block.arguments !== \"object\" || block.arguments === null) {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: `blocks[${j}].arguments must be a JSON string or object, got ${typeof block.arguments}`,\n });\n }\n if (block.id !== undefined && typeof block.id !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: `blocks[${j}].id must be a string, got ${typeof block.id}`,\n });\n }\n }\n }\n\n // blocks-vs-content/toolCalls divergence (#274 P2). When a fixture carries\n // BOTH `blocks` AND legacy `content`/`toolCalls` (allowed but unusual now\n // that blocks-only is first-class), builders stream `blocks` and IGNORE the\n // redundant `content`/`toolCalls`. If those disagree it is a silent footgun,\n // so WARN (not a hard error). Stay silent on the clean blocks-only path\n // (neither legacy field present) — that is the intended shape.\n const hasLegacyContent = typeof response.content === \"string\";\n const hasLegacyToolCalls = Array.isArray(response.toolCalls);\n if ((hasLegacyContent || hasLegacyToolCalls) && Array.isArray(response.blocks)) {\n const textBlocks = response.blocks.filter(\n (b): b is { type: \"text\"; text: string } =>\n b != null &&\n typeof b === \"object\" &&\n (b as { type?: unknown }).type === \"text\" &&\n typeof (b as { text?: unknown }).text === \"string\",\n );\n const toolCallBlockNames = response.blocks\n .filter(\n (b): b is { type: \"toolCall\"; name: string } =>\n b != null &&\n typeof b === \"object\" &&\n (b as { type?: unknown }).type === \"toolCall\" &&\n typeof (b as { name?: unknown }).name === \"string\",\n )\n .map((b) => b.name);\n\n // Text divergence: blocks' concatenated text vs legacy `content`.\n if (hasLegacyContent) {\n const blocksText = textBlocks.map((b) => b.text).join(\"\");\n if (blocksText !== response.content) {\n results.push({\n severity: \"warning\",\n fixtureIndex,\n message:\n \"blocks text diverges from content — builders stream blocks and ignore the redundant content field\",\n });\n }\n }\n\n // ToolCall divergence: blocks' ordered toolCall names vs legacy `toolCalls`.\n if (hasLegacyToolCalls) {\n const legacyNames = (response.toolCalls as Array<{ name?: unknown }>).map((tc) =>\n typeof tc?.name === \"string\" ? tc.name : undefined,\n );\n const sameNames =\n legacyNames.length === toolCallBlockNames.length &&\n legacyNames.every((n, k) => n === toolCallBlockNames[k]);\n if (!sameNames) {\n results.push({\n severity: \"warning\",\n fixtureIndex,\n message:\n \"blocks toolCalls diverge from toolCalls — builders stream blocks and ignore the redundant toolCalls field\",\n });\n }\n }\n }\n}\n\n/**\n * Human-readable form of the status range an injected error may carry.\n * Measured on Node v26: `res.writeHead` throws outside 100-999 (the injected\n * error is then lost and the caller sees a generic 500), and a 1xx status is\n * accepted but cannot terminate a response, so it hangs the request until the\n * client times out. 600-999 is unassigned, NOT invalid — Node sends it and a\n * client sees it — so a mock server allows it.\n */\nexport const INJECTED_STATUS_RANGE =\n \"an integer between 200 and 999 (1xx cannot terminate a response and would hang the client)\";\n\n/**\n * Shared by the three doors that queue an error whose `status` reaches\n * `res.writeHead`: a fixture's `response.status` (below), `POST\n * /__aimock/error`, and `LLMock.nextRequestError`.\n */\nexport function isInjectableStatus(status: unknown): status is number {\n // The typeof is for the COMPILER — it narrows `unknown` so the comparisons\n // below type-check. At runtime `Number.isInteger` already excludes every\n // non-number, so mutating it away fails nothing.\n if (typeof status !== \"number\") return false;\n return Number.isInteger(status) && status >= 200 && status <= 999;\n}\n\n/**\n * An injected error is only a valid response for endpoints that can carry an\n * error envelope. Mirrors the router's endpoint-compat table\n * (matchFixtureDiagnostic in router.ts): error responses are compatible with\n * chat / embedding / realtime* / fal / the four elevenlabs-voice* slots and\n * with requests that carry no endpoint type, but NOT with multimedia endpoints\n * (image, speech, video, transcription, …). Whether files / fine-tuning /\n * batches belong here is an open design question — not widened yet.\n */\nfunction isErrorEndpointCompatible(req: ChatCompletionRequest): boolean {\n const reqEndpoint = req._endpointType as string | undefined;\n return (\n reqEndpoint === undefined ||\n reqEndpoint === \"chat\" ||\n reqEndpoint === \"embedding\" ||\n reqEndpoint.startsWith(\"realtime\") ||\n reqEndpoint === \"fal\" ||\n reqEndpoint === \"elevenlabs-voice-design\" ||\n reqEndpoint === \"elevenlabs-voice\" ||\n reqEndpoint === \"elevenlabs-voice-get\" ||\n reqEndpoint === \"elevenlabs-voice-delete\"\n );\n}\n\n/**\n * Queue a one-shot injected error at the FRONT of `fixtures` — shared by\n * `LLMock.nextRequestError` and `POST /__aimock/error` so both doors gate and\n * consume identically. The caller validates `status` (`isInjectableStatus`).\n *\n * The fixture is consumed exactly when its error body is actually WRITTEN to a\n * client, not when its predicate is evaluated and not when its factory happens\n * to run. Handlers that await chaos claim it synchronously at selection\n * (`claimOneShotError`) so two concurrent requests cannot both pick it across\n * the chaos-latency await, and RELEASES the claim (`releaseOneShotError`) on\n * every exit that ends the request without writing that body — a terminal\n * chaos action, or a client that left during the chaos-latency delay — unless\n * the queue was cleared (`clearFixtureQueue`) in between, in which case the\n * stale claim is dropped rather than re-armed into the reset queue. The\n * claim cannot simply be deferred past the chaos gate instead: the chaos\n * config is resolved FROM the selected fixture, and a claim taken after the\n * latency await re-opens the concurrency race the claim-at-selection closes.\n * The OpenRouter `models[]` loop cannot burn it by resolving a candidate it\n * then fails past — the error is `fallthrough: false` (terminal) so the\n * candidate that selects it serves it. The factory still performs the same\n * idempotent claim for handlers that only reach the fixture via\n * `resolveResponse` without an intervening await (realtime and fal). Consuming at\n * predicate time burned the error unserved whenever a later fixture won\n * selection (e.g. a behind-the-count `turnIndex` fixture) and spliced the\n * array while the router was still iterating it, skipping the very fixture\n * that then won. The predicate gate keeps an incompatible request (an image\n * call) from selecting it at all, so the error stays pending for its intended\n * endpoint.\n */\nconst oneShotErrorFixtures = new WeakSet<Fixture>();\n\n/** True for a fixture queued by `queueOneShotError` (still armed or not). */\nexport function isOneShotError(fixture: Fixture): boolean {\n return oneShotErrorFixtures.has(fixture);\n}\n\n/**\n * Claim (consume) a one-shot error fixture. Returns true when this call\n * removed it from `fixtures`; false when another request already claimed it,\n * in which case the caller must NOT serve it and must re-select.\n */\nexport function claimOneShotError(fixtures: Fixture[], fixture: Fixture): boolean {\n const idx = fixtures.indexOf(fixture);\n if (idx === -1) return false;\n fixtures.splice(idx, 1);\n oneShotClaimGeneration.set(fixture, queueGeneration(fixtures));\n return true;\n}\n\n/**\n * Generation of each fixture queue, bumped by `clearFixtureQueue`. A one-shot\n * claim records the generation it was taken under; a release under a LATER\n * generation means the queue was cleared or reset while the claim was parked\n * (in the chaos-latency await) and the claim is stale — re-arming it would\n * plant the previous test's injection in the next test's freshly reset queue.\n * Keyed by array identity: every clear path preserves the array reference\n * (`length = 0`), so the counter follows the live queue.\n */\nconst fixtureQueueGeneration = new WeakMap<Fixture[], number>();\nconst oneShotClaimGeneration = new WeakMap<Fixture, number>();\n\nfunction queueGeneration(fixtures: Fixture[]): number {\n return fixtureQueueGeneration.get(fixtures) ?? 0;\n}\n\n/**\n * Empty a fixture queue in place (array identity preserved — the running\n * server reads this same array on every request) and invalidate every\n * outstanding one-shot claim against it, so a release that lands after the\n * clear is a no-op instead of re-arming into the emptied queue. The ONE door\n * for `POST /__aimock/reset`, `DELETE /__aimock/fixtures` and\n * `LLMock.clearFixtures`; a bare `fixtures.length = 0` misses the second half.\n */\nexport function clearFixtureQueue(fixtures: Fixture[]): void {\n fixtures.length = 0;\n fixtureQueueGeneration.set(fixtures, queueGeneration(fixtures) + 1);\n}\n\n/**\n * Re-arm a one-shot error that was claimed for a request which then ended\n * WITHOUT writing the error body — a terminal chaos action (drop / disconnect\n * / rateLimit / malformed) or a client that hung up during the chaos-latency\n * delay. Without this the injection is burned unserved and the NEXT request\n * gets a 200, which is exactly the outcome `nextRequestError` promises not to\n * produce. A no-op for a fixture that is not a one-shot, and for one still\n * present in `fixtures` (never claimed, or already re-armed) so a double\n * release cannot duplicate the injection — and for a claim taken before the\n * queue was last cleared, so a reset that raced the request stays a reset.\n */\nexport function releaseOneShotError(fixtures: Fixture[], fixture: Fixture | null): void {\n if (fixture === null || !oneShotErrorFixtures.has(fixture)) return;\n if (fixtures.includes(fixture)) return;\n // Claimed under an earlier generation: the queue was cleared/reset while\n // this request was parked. The clear disarmed the injection — stay cleared.\n if (oneShotClaimGeneration.get(fixture) !== queueGeneration(fixtures)) return;\n // Front, matching `queueOneShotError`: the one-shot wins registration-order\n // ties against everything else, exactly as it did before it was claimed.\n fixtures.unshift(fixture);\n}\n\nexport function queueOneShotError(\n fixtures: Fixture[],\n status: number,\n errorBody?: { message?: string; type?: string; code?: string },\n): Fixture {\n const errorResponse: FixtureResponse = {\n error: {\n message: errorBody?.message ?? \"Injected error\",\n type: errorBody?.type ?? \"server_error\",\n code: errorBody?.code,\n },\n status,\n // Terminal in the OpenRouter `models[]` loop: the candidate that selects\n // (and thereby claims) the one-shot serves it instead of failing past it.\n fallthrough: false,\n };\n const fixture: Fixture = {\n match: { predicate: isErrorEndpointCompatible },\n response: () => {\n // Idempotent: a no-op when the handler already claimed at selection.\n claimOneShotError(fixtures, fixture);\n return errorResponse;\n },\n };\n oneShotErrorFixtures.add(fixture);\n // Insert at front so it wins registration-order ties against everything else.\n fixtures.unshift(fixture);\n return fixture;\n}\n\nexport function validateFixtures(\n fixtures: Fixture[],\n liveOptions?: LiveOptions,\n): 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 if (\n typeof response !== \"function\" &&\n (isLiveResponse(response) || f.match.endpoint === \"openai-live\")\n ) {\n try {\n normalizeLiveFixture(response, liveOptions);\n } catch (error) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n // Skip response-shape validation for function responses — they are\n // evaluated at runtime so we cannot statically inspect them.\n if (typeof response === \"function\") {\n // Still validate match fields and numeric options below.\n } else {\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 !isJSONResponse(response) &&\n !isLiveResponse(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, video, json, or live)\",\n });\n }\n\n // When a non-empty ordered `blocks` array is present, the builders stream\n // `blocks` and IGNORE the legacy `content` mirror (see validateBlocks's\n // divergence note + isContentWithToolCallsResponse's BLOCKS-ONLY clause).\n // So an empty-string `content` is harmless in that case and must NOT raise\n // the \"content is empty string\" hard error. Fixtures WITHOUT blocks keep\n // the error (an empty content with no blocks produces no output).\n const hasNonEmptyBlocks =\n Array.isArray((response as { blocks?: unknown }).blocks) &&\n (response as { blocks: unknown[] }).blocks.length > 0;\n\n // Text response checks\n if (isTextResponse(response)) {\n if (response.content === \"\" && !hasNonEmptyBlocks) {\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 // The guard now also matches a BLOCKS-ONLY fixture (non-empty `blocks`,\n // no `content`/`toolCalls`). For that shape the content/toolCalls checks\n // below don't apply (and `content`/`toolCalls` are undefined) — the\n // ordered `blocks` array is validated separately by `validateBlocks`\n // immediately after this block. So gate the legacy field checks on the\n // fields actually being present, mirroring the builders' branch-on-blocks.\n if (typeof response.content === \"string\") {\n if (response.content === \"\" && !hasNonEmptyBlocks) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"content is empty string\",\n });\n }\n }\n if (Array.isArray(response.toolCalls)) {\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 validateReasoning(response, i, results);\n validateWebSearches(response, i, results);\n }\n\n // Optional ordered `blocks` checks — validated whenever present on the\n // response, regardless of which content/toolCalls guard matched, so a\n // malformed blocks array is rejected at LOAD rather than at dispatch.\n validateBlocks(\n response as { blocks?: unknown; content?: unknown; toolCalls?: unknown },\n i,\n 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 validateWebSearches(response, i, results);\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 // Shared with `POST /__aimock/error` and `LLMock.nextRequestError`\n // (see `isInjectableStatus` above): a fixture's status lands on the same\n // `res.writeHead`, so it is accepted or rejected identically.\n if (response.status !== undefined && !isInjectableStatus(response.status)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `error status ${response.status} is not a valid HTTP status code — must be ${INJECTED_STATUS_RANGE}`,\n });\n }\n // `fallthrough` gates OpenRouter `models[]` failover and is read as\n // `!== false`, so a non-boolean (`\"false\"`, 0, null) would silently\n // fall through — the OPPOSITE of the intended terminal behavior. Reject\n // it at load so the fail-closed half cannot quietly fail open.\n if (response.fallthrough !== undefined && typeof response.fallthrough !== \"boolean\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `error.fallthrough must be a boolean, got ${typeof response.fallthrough}`,\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 // Audio response checks — validate object-form audio\n if (isAudioResponse(response) && typeof response.audio === \"object\") {\n const audioObj = response.audio;\n if (typeof audioObj.b64Json !== \"string\" || audioObj.b64Json === \"\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"audio.b64Json must be a non-empty string\",\n });\n }\n if (audioObj.contentType !== undefined && typeof audioObj.contentType !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `audio.contentType must be a string, got ${typeof audioObj.contentType}`,\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 // Numeric token fields must be numbers, but the OpenRouter cost\n // sub-fields carry non-numeric shapes (see ResponseOverrides.usage\n // in types.ts): cost_details / prompt_tokens_details /\n // completion_tokens_details are objects and is_byok is a boolean.\n // Validate each per its documented type so cost-scripting fixtures\n // are not rejected with a misleading \"must be a number\" error.\n // Known numeric inner scalars per object field (see\n // OpenRouterUsageExtras in types.ts). Validating only these keeps\n // the index-signature ([key: string]: unknown) escape hatch open\n // for forward-compat fields while still catching malformed values\n // on the documented ones — otherwise a bad inner scalar (e.g.\n // cost_details.upstream_inference_cost: \"abc\") passes load-time\n // validation and only blows up at replay in the canonical SDK.\n const numericInnerFields: Record<string, readonly string[]> = {\n cost_details: [\n \"upstream_inference_cost\",\n \"upstream_inference_prompt_cost\",\n \"upstream_inference_completions_cost\",\n ],\n prompt_tokens_details: [\"cached_tokens\", \"cache_write_tokens\", \"audio_tokens\"],\n completion_tokens_details: [\"reasoning_tokens\"],\n };\n const objectUsageFields = new Set(Object.keys(numericInnerFields));\n for (const key of Object.keys(r.usage)) {\n const val = (r.usage as Record<string, unknown>)[key];\n if (val === undefined) continue;\n if (objectUsageFields.has(key)) {\n if (typeof val !== \"object\" || val === null || Array.isArray(val)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"usage.${key}\" must be an object, got ${\n Array.isArray(val) ? \"array\" : typeof val\n }`,\n });\n } else {\n // Type-check the documented inner scalars; each is optional\n // (required-if-present), so only validate keys that are set.\n const inner = val as Record<string, unknown>;\n for (const innerKey of numericInnerFields[key]) {\n const innerVal = inner[innerKey];\n if (innerVal !== undefined && typeof innerVal !== \"number\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"usage.${key}.${innerKey}\" must be a number, got ${typeof innerVal}`,\n });\n }\n }\n }\n } else if (key === \"is_byok\") {\n if (typeof val !== \"boolean\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"usage.is_byok\" must be a boolean, got ${typeof val}`,\n });\n }\n } else if (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 } // end: skip response-shape validation for function responses\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 // EVERY chaos field is routed through the ONE chaos table, so a fixture\n // cannot accept a value the CLI, the header and the runtime reject. The\n // hand-rolled `< 0 || > 1` checks this replaces let `NaN`, `-0` and a\n // numeric string (`dropRate: \"0.5\"`) validate clean, and `latencyMs: 1.5`\n // through; the runtime then rejected the fixture value on every request.\n // A fixture is typed data, like the control API's JSON body: the field\n // must be a number. A numeric STRING is a type error here, not a wire\n // spelling to be parsed — the header API is the surface that takes text.\n const ch = f.chaos as Record<string, unknown>;\n for (const field of CHAOS_FIELD_NAMES) {\n const value = ch[field];\n if (value === undefined) continue;\n const accepted = typeof value === \"number\" ? parseChaosField(field, value) : undefined;\n if (accepted === undefined) {\n const shape = CHAOS_FIELDS[field].integer ? \"a whole number of ms\" : \"a number\";\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `chaos.${field} must be ${shape} between 0 and ${CHAOS_FIELDS[field].max}`,\n });\n }\n }\n }\n\n // Match field type checks\n if (f.match.turnIndex !== undefined) {\n if (\n typeof f.match.turnIndex !== \"number\" ||\n f.match.turnIndex < 0 ||\n !Number.isInteger(f.match.turnIndex)\n ) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"match.turnIndex must be a non-negative integer\",\n });\n }\n }\n if (f.match.sequenceIndex !== undefined) {\n if (\n typeof f.match.sequenceIndex !== \"number\" ||\n f.match.sequenceIndex < 0 ||\n !Number.isInteger(f.match.sequenceIndex)\n ) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"match.sequenceIndex must be a non-negative integer\",\n });\n }\n }\n if (f.match.hasToolResult !== undefined && typeof f.match.hasToolResult !== \"boolean\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `match.hasToolResult must be a boolean, got ${typeof f.match.hasToolResult}`,\n });\n }\n if (f.match.toolResultContains !== undefined) {\n if (typeof f.match.toolResultContains !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `match.toolResultContains must be a string, got ${typeof f.match.toolResultContains}`,\n });\n } else if (f.match.toolResultContains.length === 0) {\n // An empty substring is always contained, so the gate would silently\n // act as \"last message is any tool result\" — reject it as an authoring\n // mistake rather than let it shadow later fixtures.\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"match.toolResultContains must be a non-empty string\",\n });\n }\n }\n if (f.match.systemMessage !== undefined) {\n const sm = f.match.systemMessage;\n if (typeof sm === \"string\") {\n // ok\n } else if (Array.isArray(sm)) {\n if (sm.length === 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `match.systemMessage array must contain at least one substring`,\n });\n } else {\n for (let j = 0; j < sm.length; j++) {\n if (typeof sm[j] !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `match.systemMessage[${j}] must be a string, got ${typeof sm[j]}`,\n });\n }\n }\n }\n } else {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `match.systemMessage must be a string or string[], got ${typeof sm}`,\n });\n }\n }\n if (f.match.context !== undefined && typeof f.match.context !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `match.context must be a string, got ${typeof f.match.context}`,\n });\n }\n\n // --- Warning checks ---\n\n // Duplicate userMessage shadowing — two fixtures are only genuine\n // duplicates when they would match the SAME requests, so the dedup key must\n // include EVERY match discriminator the router (matchFixtureDiagnostic in\n // router.ts) actually gates on: userMessage, systemMessage, inputText,\n // toolCallId, toolResultContains, toolName, model, responseFormat, endpoint,\n // context, sequenceIndex, turnIndex, and hasToolResult. Omitting any of these\n // (the old key only carried turnIndex/hasToolResult/toolResultContains/\n // sequenceIndex/context) flags two legitimately-distinct fixtures — e.g. two\n // that differ ONLY in toolCallId or model — as false duplicates.\n //\n // `predicate` is a function and cannot be compared for equivalence, so it is\n // never folded into a shared key: a fixture carrying a predicate is treated\n // as unconditionally distinct (keyed by its own index) so it neither shadows\n // nor is shadowed by another fixture on the userMessage axis.\n //\n // Values are serialised kind-aware so RegExp / string[] matchers do not\n // collide (a template literal would coerce a RegExp to its source and an\n // array via join, losing the distinction) — mirroring describeMatch in\n // router.ts.\n const um = f.match.userMessage;\n if (typeof um === \"string\" && um) {\n const m = f.match;\n const dedupKey =\n m.predicate !== undefined\n ? `predicate:${i}`\n : [\n serializeMatcher(m.userMessage),\n serializeMatcher(m.systemMessage),\n serializeMatcher(m.inputText),\n m.toolCallId,\n m.toolResultContains,\n m.toolName,\n serializeMatcher(m.model),\n m.responseFormat,\n m.endpoint,\n m.context,\n m.sequenceIndex,\n m.turnIndex,\n m.hasToolResult,\n ]\n .map((v) => (v === undefined ? \"\" : String(v)))\n .join(\"|\");\n const prev = seenUserMessages.get(dedupKey);\n if (prev !== undefined) {\n const ref: ValidationRef = {\n rule: \"duplicate-user-message\",\n userMessage: um,\n shadows: prev,\n };\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: renderValidationRef(ref, String),\n ref,\n });\n } else {\n seenUserMessages.set(dedupKey, 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.context !== undefined ||\n match.userMessage !== undefined ||\n match.systemMessage !== undefined ||\n match.inputText !== undefined ||\n match.responseFormat !== undefined ||\n match.toolCallId !== undefined ||\n match.toolResultContains !== undefined ||\n match.toolName !== undefined ||\n match.model !== undefined ||\n match.predicate !== undefined ||\n match.turnIndex !== undefined ||\n match.sequenceIndex !== undefined ||\n match.hasToolResult !== undefined;\n\n if (!hasDiscriminator && i < fixtures.length - 1) {\n const ref: ValidationRef = {\n rule: \"catch-all-not-last\",\n shadowsFrom: i + 1,\n shadowsThrough: fixtures.length - 1,\n };\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: renderValidationRef(ref, String),\n ref,\n });\n }\n }\n\n return results;\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const require_helpers = require('./helpers.cjs');
|
|
2
|
-
const require_router = require('./router.cjs');
|
|
3
2
|
const require_sse_writer = require('./sse-writer.cjs');
|
|
4
3
|
const require_chaos = require('./chaos.cjs');
|
|
4
|
+
const require_router = require('./router.cjs');
|
|
5
5
|
const require_recorder = require('./recorder.cjs');
|
|
6
6
|
|
|
7
7
|
//#region src/gemini-embeddings.ts
|
|
@@ -64,7 +64,7 @@ async function handleGeminiEmbedContent(req, res, raw, model, fixtures, journal,
|
|
|
64
64
|
journal.incrementFixtureMatchCount(fixture, fixtures, testId);
|
|
65
65
|
logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);
|
|
66
66
|
} else logger.debug(`No fixture matched for request`);
|
|
67
|
-
if (require_chaos.
|
|
67
|
+
if (await require_chaos.applyChaosAsync(res, fixture, defaults.chaos, req.headers, req.url, journal, {
|
|
68
68
|
method: req.method ?? "POST",
|
|
69
69
|
path,
|
|
70
70
|
headers: require_helpers.flattenHeaders(req.headers),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gemini-embeddings.cjs","names":["flattenHeaders","isJsonObject","getContext","getTestId","matchFixtureDiagnostic","applyChaos","resolveResponse","isErrorResponse","isEmbeddingResponse","resolveStrictMode","strictNoMatchMessage","strictNoMatchLogLine","strictOverrideField","proxyAndRecord","generateDeterministicEmbedding"],"sources":["../src/gemini-embeddings.ts"],"sourcesContent":["/**\n * Google Gemini embedContent API support.\n *\n * Handles POST /v1beta/models/{model}:embedContent requests. Translates\n * incoming Gemini embedding requests into the ChatCompletionRequest format\n * used by the fixture router (with _endpointType: \"embedding\"), and converts\n * fixture responses back into the Gemini embedContent response format.\n *\n * Falls back to generating a deterministic embedding from the input text hash\n * when no fixture matches — same strategy as the OpenAI embeddings handler.\n */\n\nimport type * as http from \"node:http\";\nimport type {\n ChatCompletionRequest,\n Fixture,\n HandlerDefaults,\n RecordProviderKey,\n} from \"./types.js\";\nimport {\n isEmbeddingResponse,\n isErrorResponse,\n generateDeterministicEmbedding,\n flattenHeaders,\n isJsonObject,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n getContext,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaos } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\n// ─── Gemini embedContent request types ────────────────────────────────────\n\ninterface GeminiEmbedContentPart {\n text?: string;\n}\n\ninterface GeminiEmbedContentRequest {\n content?: { parts?: GeminiEmbedContentPart[] };\n model?: string;\n taskType?: string;\n title?: string;\n outputDimensionality?: number;\n [key: string]: unknown;\n}\n\n// ─── Gemini embedContent response type ────────────────────────────────────\n\ninterface GeminiEmbedContentResponse {\n embedding: {\n values: number[];\n };\n}\n\n// ─── Default embedding dimensions for Gemini ──────────────────────────────\n\nconst DEFAULT_GEMINI_EMBEDDING_DIMENSIONS = 768;\n\n// ─── Request handler ───────────────────────────────────────────────────────\n\nexport async function handleGeminiEmbedContent(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n model: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n providerKey: RecordProviderKey = \"gemini\",\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let embedReq: GeminiEmbedContentRequest;\n try {\n embedReq = JSON.parse(raw) as GeminiEmbedContentRequest;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? `/v1beta/models/${model}:embedContent`,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: `Malformed JSON body: ${detail}`,\n code: 400,\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n\n // Reject bodies that parsed but are not a JSON object (e.g. `null`) before\n // touching fields — otherwise `embedReq.content` throws a TypeError that\n // surfaces as a 500 instead of a 400.\n if (!isJsonObject(embedReq)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? `/v1beta/models/${model}:embedContent`,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: \"Request body must be a JSON object\",\n code: 400,\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n\n // Extract text from content.parts\n const parts = embedReq.content?.parts ?? [];\n const inputText = parts\n .filter((p) => p.text !== undefined)\n .map((p) => p.text!)\n .join(\" \");\n\n // Build a synthetic ChatCompletionRequest for the fixture router.\n // Uses _endpointType: \"embedding\" and embeddingInput to share fixtures\n // with OpenAI embeddings.\n const syntheticReq: ChatCompletionRequest = {\n model: embedReq.model ?? model,\n messages: [],\n embeddingInput: inputText,\n _endpointType: \"embedding\",\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n const path = req.url ?? `/v1beta/models/${model}:embedContent`;\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n logger.debug(`No fixture matched for request`);\n }\n\n if (\n applyChaos(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n {\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (fixture) {\n const response = await resolveResponse(fixture, syntheticReq);\n\n // Error response\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n const geminiError = {\n error: {\n code: status,\n message: response.error.message,\n status: response.error.type ?? \"ERROR\",\n },\n };\n writeErrorResponse(res, status, JSON.stringify(geminiError), {\n retryAfter: response.retryAfter,\n });\n return;\n }\n\n // Embedding response — use the fixture's embedding values\n if (isEmbeddingResponse(response)) {\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n const body: GeminiEmbedContentResponse = {\n embedding: {\n values: [...response.embedding],\n },\n };\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n return;\n }\n\n // Fixture matched but response type is not compatible with embeddings\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message:\n \"Fixture response did not match any known embedding type (must have embedding or error)\",\n code: 500,\n status: \"INTERNAL\",\n },\n }),\n );\n return;\n }\n\n // No fixture match — check strict mode first, then try proxy\n const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n if (effectiveStrict) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n logger.error(strictNoMatchLogLine(req.method ?? \"POST\", path, skippedBySequenceOrTurn));\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\n JSON.stringify({\n error: {\n message: strictMessage,\n code: 503,\n status: \"UNAVAILABLE\",\n },\n }),\n );\n return;\n }\n\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n providerKey,\n path,\n fixtures,\n defaults,\n raw,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome !== \"not_configured\") {\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return;\n }\n }\n\n // No fixture match — generate deterministic embedding from input text\n const dimensions = embedReq.outputDimensionality ?? DEFAULT_GEMINI_EMBEDDING_DIMENSIONS;\n const embedding = generateDeterministicEmbedding(inputText, dimensions);\n\n logger.warn(\n `No embedding fixture matched for \"${inputText.slice(0, 80)}\" — returning deterministic fallback`,\n );\n\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture: null },\n });\n\n const body: GeminiEmbedContentResponse = {\n embedding: {\n values: embedding,\n },\n };\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"gemini-embeddings.cjs","names":["flattenHeaders","isJsonObject","getContext","getTestId","matchFixtureDiagnostic","applyChaosAsync","resolveResponse","isErrorResponse","isEmbeddingResponse","resolveStrictMode","strictNoMatchMessage","strictNoMatchLogLine","strictOverrideField","proxyAndRecord","generateDeterministicEmbedding"],"sources":["../src/gemini-embeddings.ts"],"sourcesContent":["/**\n * Google Gemini embedContent API support.\n *\n * Handles POST /v1beta/models/{model}:embedContent requests. Translates\n * incoming Gemini embedding requests into the ChatCompletionRequest format\n * used by the fixture router (with _endpointType: \"embedding\"), and converts\n * fixture responses back into the Gemini embedContent response format.\n *\n * Falls back to generating a deterministic embedding from the input text hash\n * when no fixture matches — same strategy as the OpenAI embeddings handler.\n */\n\nimport type * as http from \"node:http\";\nimport type {\n ChatCompletionRequest,\n Fixture,\n HandlerDefaults,\n RecordProviderKey,\n} from \"./types.js\";\nimport {\n isEmbeddingResponse,\n isErrorResponse,\n generateDeterministicEmbedding,\n flattenHeaders,\n isJsonObject,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n getContext,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaosAsync } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\n// ─── Gemini embedContent request types ────────────────────────────────────\n\ninterface GeminiEmbedContentPart {\n text?: string;\n}\n\ninterface GeminiEmbedContentRequest {\n content?: { parts?: GeminiEmbedContentPart[] };\n model?: string;\n taskType?: string;\n title?: string;\n outputDimensionality?: number;\n [key: string]: unknown;\n}\n\n// ─── Gemini embedContent response type ────────────────────────────────────\n\ninterface GeminiEmbedContentResponse {\n embedding: {\n values: number[];\n };\n}\n\n// ─── Default embedding dimensions for Gemini ──────────────────────────────\n\nconst DEFAULT_GEMINI_EMBEDDING_DIMENSIONS = 768;\n\n// ─── Request handler ───────────────────────────────────────────────────────\n\nexport async function handleGeminiEmbedContent(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n model: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n providerKey: RecordProviderKey = \"gemini\",\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let embedReq: GeminiEmbedContentRequest;\n try {\n embedReq = JSON.parse(raw) as GeminiEmbedContentRequest;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? `/v1beta/models/${model}:embedContent`,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: `Malformed JSON body: ${detail}`,\n code: 400,\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n\n // Reject bodies that parsed but are not a JSON object (e.g. `null`) before\n // touching fields — otherwise `embedReq.content` throws a TypeError that\n // surfaces as a 500 instead of a 400.\n if (!isJsonObject(embedReq)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? `/v1beta/models/${model}:embedContent`,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: \"Request body must be a JSON object\",\n code: 400,\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n\n // Extract text from content.parts\n const parts = embedReq.content?.parts ?? [];\n const inputText = parts\n .filter((p) => p.text !== undefined)\n .map((p) => p.text!)\n .join(\" \");\n\n // Build a synthetic ChatCompletionRequest for the fixture router.\n // Uses _endpointType: \"embedding\" and embeddingInput to share fixtures\n // with OpenAI embeddings.\n const syntheticReq: ChatCompletionRequest = {\n model: embedReq.model ?? model,\n messages: [],\n embeddingInput: inputText,\n _endpointType: \"embedding\",\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n const path = req.url ?? `/v1beta/models/${model}:embedContent`;\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n logger.debug(`No fixture matched for request`);\n }\n\n if (\n await applyChaosAsync(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n {\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (fixture) {\n const response = await resolveResponse(fixture, syntheticReq);\n\n // Error response\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n const geminiError = {\n error: {\n code: status,\n message: response.error.message,\n status: response.error.type ?? \"ERROR\",\n },\n };\n writeErrorResponse(res, status, JSON.stringify(geminiError), {\n retryAfter: response.retryAfter,\n });\n return;\n }\n\n // Embedding response — use the fixture's embedding values\n if (isEmbeddingResponse(response)) {\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n const body: GeminiEmbedContentResponse = {\n embedding: {\n values: [...response.embedding],\n },\n };\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n return;\n }\n\n // Fixture matched but response type is not compatible with embeddings\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message:\n \"Fixture response did not match any known embedding type (must have embedding or error)\",\n code: 500,\n status: \"INTERNAL\",\n },\n }),\n );\n return;\n }\n\n // No fixture match — check strict mode first, then try proxy\n const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n if (effectiveStrict) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n logger.error(strictNoMatchLogLine(req.method ?? \"POST\", path, skippedBySequenceOrTurn));\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\n JSON.stringify({\n error: {\n message: strictMessage,\n code: 503,\n status: \"UNAVAILABLE\",\n },\n }),\n );\n return;\n }\n\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n providerKey,\n path,\n fixtures,\n defaults,\n raw,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome !== \"not_configured\") {\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return;\n }\n }\n\n // No fixture match — generate deterministic embedding from input text\n const dimensions = embedReq.outputDimensionality ?? DEFAULT_GEMINI_EMBEDDING_DIMENSIONS;\n const embedding = generateDeterministicEmbedding(inputText, dimensions);\n\n logger.warn(\n `No embedding fixture matched for \"${inputText.slice(0, 80)}\" — returning deterministic fallback`,\n );\n\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture: null },\n });\n\n const body: GeminiEmbedContentResponse = {\n embedding: {\n values: embedding,\n },\n };\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { flattenHeaders, generateDeterministicEmbedding, getContext, getTestId, isEmbeddingResponse, isErrorResponse, isJsonObject, resolveResponse, resolveStrictMode, strictNoMatchLogLine, strictNoMatchMessage, strictOverrideField } from "./helpers.js";
|
|
2
|
-
import { matchFixtureDiagnostic } from "./router.js";
|
|
3
2
|
import { writeErrorResponse } from "./sse-writer.js";
|
|
4
|
-
import {
|
|
3
|
+
import { applyChaosAsync } from "./chaos.js";
|
|
4
|
+
import { matchFixtureDiagnostic } from "./router.js";
|
|
5
5
|
import { proxyAndRecord } from "./recorder.js";
|
|
6
6
|
|
|
7
7
|
//#region src/gemini-embeddings.ts
|
|
@@ -64,7 +64,7 @@ async function handleGeminiEmbedContent(req, res, raw, model, fixtures, journal,
|
|
|
64
64
|
journal.incrementFixtureMatchCount(fixture, fixtures, testId);
|
|
65
65
|
logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);
|
|
66
66
|
} else logger.debug(`No fixture matched for request`);
|
|
67
|
-
if (
|
|
67
|
+
if (await applyChaosAsync(res, fixture, defaults.chaos, req.headers, req.url, journal, {
|
|
68
68
|
method: req.method ?? "POST",
|
|
69
69
|
path,
|
|
70
70
|
headers: flattenHeaders(req.headers),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gemini-embeddings.js","names":[],"sources":["../src/gemini-embeddings.ts"],"sourcesContent":["/**\n * Google Gemini embedContent API support.\n *\n * Handles POST /v1beta/models/{model}:embedContent requests. Translates\n * incoming Gemini embedding requests into the ChatCompletionRequest format\n * used by the fixture router (with _endpointType: \"embedding\"), and converts\n * fixture responses back into the Gemini embedContent response format.\n *\n * Falls back to generating a deterministic embedding from the input text hash\n * when no fixture matches — same strategy as the OpenAI embeddings handler.\n */\n\nimport type * as http from \"node:http\";\nimport type {\n ChatCompletionRequest,\n Fixture,\n HandlerDefaults,\n RecordProviderKey,\n} from \"./types.js\";\nimport {\n isEmbeddingResponse,\n isErrorResponse,\n generateDeterministicEmbedding,\n flattenHeaders,\n isJsonObject,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n getContext,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaos } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\n// ─── Gemini embedContent request types ────────────────────────────────────\n\ninterface GeminiEmbedContentPart {\n text?: string;\n}\n\ninterface GeminiEmbedContentRequest {\n content?: { parts?: GeminiEmbedContentPart[] };\n model?: string;\n taskType?: string;\n title?: string;\n outputDimensionality?: number;\n [key: string]: unknown;\n}\n\n// ─── Gemini embedContent response type ────────────────────────────────────\n\ninterface GeminiEmbedContentResponse {\n embedding: {\n values: number[];\n };\n}\n\n// ─── Default embedding dimensions for Gemini ──────────────────────────────\n\nconst DEFAULT_GEMINI_EMBEDDING_DIMENSIONS = 768;\n\n// ─── Request handler ───────────────────────────────────────────────────────\n\nexport async function handleGeminiEmbedContent(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n model: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n providerKey: RecordProviderKey = \"gemini\",\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let embedReq: GeminiEmbedContentRequest;\n try {\n embedReq = JSON.parse(raw) as GeminiEmbedContentRequest;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? `/v1beta/models/${model}:embedContent`,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: `Malformed JSON body: ${detail}`,\n code: 400,\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n\n // Reject bodies that parsed but are not a JSON object (e.g. `null`) before\n // touching fields — otherwise `embedReq.content` throws a TypeError that\n // surfaces as a 500 instead of a 400.\n if (!isJsonObject(embedReq)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? `/v1beta/models/${model}:embedContent`,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: \"Request body must be a JSON object\",\n code: 400,\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n\n // Extract text from content.parts\n const parts = embedReq.content?.parts ?? [];\n const inputText = parts\n .filter((p) => p.text !== undefined)\n .map((p) => p.text!)\n .join(\" \");\n\n // Build a synthetic ChatCompletionRequest for the fixture router.\n // Uses _endpointType: \"embedding\" and embeddingInput to share fixtures\n // with OpenAI embeddings.\n const syntheticReq: ChatCompletionRequest = {\n model: embedReq.model ?? model,\n messages: [],\n embeddingInput: inputText,\n _endpointType: \"embedding\",\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n const path = req.url ?? `/v1beta/models/${model}:embedContent`;\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n logger.debug(`No fixture matched for request`);\n }\n\n if (\n applyChaos(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n {\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (fixture) {\n const response = await resolveResponse(fixture, syntheticReq);\n\n // Error response\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n const geminiError = {\n error: {\n code: status,\n message: response.error.message,\n status: response.error.type ?? \"ERROR\",\n },\n };\n writeErrorResponse(res, status, JSON.stringify(geminiError), {\n retryAfter: response.retryAfter,\n });\n return;\n }\n\n // Embedding response — use the fixture's embedding values\n if (isEmbeddingResponse(response)) {\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n const body: GeminiEmbedContentResponse = {\n embedding: {\n values: [...response.embedding],\n },\n };\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n return;\n }\n\n // Fixture matched but response type is not compatible with embeddings\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message:\n \"Fixture response did not match any known embedding type (must have embedding or error)\",\n code: 500,\n status: \"INTERNAL\",\n },\n }),\n );\n return;\n }\n\n // No fixture match — check strict mode first, then try proxy\n const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n if (effectiveStrict) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n logger.error(strictNoMatchLogLine(req.method ?? \"POST\", path, skippedBySequenceOrTurn));\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\n JSON.stringify({\n error: {\n message: strictMessage,\n code: 503,\n status: \"UNAVAILABLE\",\n },\n }),\n );\n return;\n }\n\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n providerKey,\n path,\n fixtures,\n defaults,\n raw,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome !== \"not_configured\") {\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return;\n }\n }\n\n // No fixture match — generate deterministic embedding from input text\n const dimensions = embedReq.outputDimensionality ?? DEFAULT_GEMINI_EMBEDDING_DIMENSIONS;\n const embedding = generateDeterministicEmbedding(inputText, dimensions);\n\n logger.warn(\n `No embedding fixture matched for \"${inputText.slice(0, 80)}\" — returning deterministic fallback`,\n );\n\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture: null },\n });\n\n const body: GeminiEmbedContentResponse = {\n embedding: {\n values: embedding,\n },\n };\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"gemini-embeddings.js","names":[],"sources":["../src/gemini-embeddings.ts"],"sourcesContent":["/**\n * Google Gemini embedContent API support.\n *\n * Handles POST /v1beta/models/{model}:embedContent requests. Translates\n * incoming Gemini embedding requests into the ChatCompletionRequest format\n * used by the fixture router (with _endpointType: \"embedding\"), and converts\n * fixture responses back into the Gemini embedContent response format.\n *\n * Falls back to generating a deterministic embedding from the input text hash\n * when no fixture matches — same strategy as the OpenAI embeddings handler.\n */\n\nimport type * as http from \"node:http\";\nimport type {\n ChatCompletionRequest,\n Fixture,\n HandlerDefaults,\n RecordProviderKey,\n} from \"./types.js\";\nimport {\n isEmbeddingResponse,\n isErrorResponse,\n generateDeterministicEmbedding,\n flattenHeaders,\n isJsonObject,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n getContext,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaosAsync } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\n// ─── Gemini embedContent request types ────────────────────────────────────\n\ninterface GeminiEmbedContentPart {\n text?: string;\n}\n\ninterface GeminiEmbedContentRequest {\n content?: { parts?: GeminiEmbedContentPart[] };\n model?: string;\n taskType?: string;\n title?: string;\n outputDimensionality?: number;\n [key: string]: unknown;\n}\n\n// ─── Gemini embedContent response type ────────────────────────────────────\n\ninterface GeminiEmbedContentResponse {\n embedding: {\n values: number[];\n };\n}\n\n// ─── Default embedding dimensions for Gemini ──────────────────────────────\n\nconst DEFAULT_GEMINI_EMBEDDING_DIMENSIONS = 768;\n\n// ─── Request handler ───────────────────────────────────────────────────────\n\nexport async function handleGeminiEmbedContent(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n model: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n providerKey: RecordProviderKey = \"gemini\",\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let embedReq: GeminiEmbedContentRequest;\n try {\n embedReq = JSON.parse(raw) as GeminiEmbedContentRequest;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? `/v1beta/models/${model}:embedContent`,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: `Malformed JSON body: ${detail}`,\n code: 400,\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n\n // Reject bodies that parsed but are not a JSON object (e.g. `null`) before\n // touching fields — otherwise `embedReq.content` throws a TypeError that\n // surfaces as a 500 instead of a 400.\n if (!isJsonObject(embedReq)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? `/v1beta/models/${model}:embedContent`,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: \"Request body must be a JSON object\",\n code: 400,\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n\n // Extract text from content.parts\n const parts = embedReq.content?.parts ?? [];\n const inputText = parts\n .filter((p) => p.text !== undefined)\n .map((p) => p.text!)\n .join(\" \");\n\n // Build a synthetic ChatCompletionRequest for the fixture router.\n // Uses _endpointType: \"embedding\" and embeddingInput to share fixtures\n // with OpenAI embeddings.\n const syntheticReq: ChatCompletionRequest = {\n model: embedReq.model ?? model,\n messages: [],\n embeddingInput: inputText,\n _endpointType: \"embedding\",\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n const path = req.url ?? `/v1beta/models/${model}:embedContent`;\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n logger.debug(`No fixture matched for request`);\n }\n\n if (\n await applyChaosAsync(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n {\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (fixture) {\n const response = await resolveResponse(fixture, syntheticReq);\n\n // Error response\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n const geminiError = {\n error: {\n code: status,\n message: response.error.message,\n status: response.error.type ?? \"ERROR\",\n },\n };\n writeErrorResponse(res, status, JSON.stringify(geminiError), {\n retryAfter: response.retryAfter,\n });\n return;\n }\n\n // Embedding response — use the fixture's embedding values\n if (isEmbeddingResponse(response)) {\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n const body: GeminiEmbedContentResponse = {\n embedding: {\n values: [...response.embedding],\n },\n };\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n return;\n }\n\n // Fixture matched but response type is not compatible with embeddings\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message:\n \"Fixture response did not match any known embedding type (must have embedding or error)\",\n code: 500,\n status: \"INTERNAL\",\n },\n }),\n );\n return;\n }\n\n // No fixture match — check strict mode first, then try proxy\n const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n if (effectiveStrict) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n logger.error(strictNoMatchLogLine(req.method ?? \"POST\", path, skippedBySequenceOrTurn));\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\n JSON.stringify({\n error: {\n message: strictMessage,\n code: 503,\n status: \"UNAVAILABLE\",\n },\n }),\n );\n return;\n }\n\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n providerKey,\n path,\n fixtures,\n defaults,\n raw,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome !== \"not_configured\") {\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return;\n }\n }\n\n // No fixture match — generate deterministic embedding from input text\n const dimensions = embedReq.outputDimensionality ?? DEFAULT_GEMINI_EMBEDDING_DIMENSIONS;\n const embedding = generateDeterministicEmbedding(inputText, dimensions);\n\n logger.warn(\n `No embedding fixture matched for \"${inputText.slice(0, 80)}\" — returning deterministic fallback`,\n );\n\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture: null },\n });\n\n const body: GeminiEmbedContentResponse = {\n embedding: {\n values: embedding,\n },\n };\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n}\n"],"mappings":"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"}
|