@copilotkit/aimock 1.40.0 → 1.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +28 -1
  4. package/README.md +5 -3
  5. package/dist/a2a-mock.cjs +18 -11
  6. package/dist/a2a-mock.cjs.map +1 -1
  7. package/dist/a2a-mock.d.cts +1 -0
  8. package/dist/a2a-mock.d.cts.map +1 -1
  9. package/dist/a2a-mock.d.ts +1 -0
  10. package/dist/a2a-mock.d.ts.map +1 -1
  11. package/dist/a2a-mock.js +18 -11
  12. package/dist/a2a-mock.js.map +1 -1
  13. package/dist/agui-handler.d.cts.map +1 -1
  14. package/dist/agui-handler.d.ts.map +1 -1
  15. package/dist/agui-mock.cjs +2 -0
  16. package/dist/agui-mock.cjs.map +1 -1
  17. package/dist/agui-mock.d.cts.map +1 -1
  18. package/dist/agui-mock.d.ts.map +1 -1
  19. package/dist/agui-mock.js +2 -0
  20. package/dist/agui-mock.js.map +1 -1
  21. package/dist/bedrock-converse.cjs +36 -2
  22. package/dist/bedrock-converse.cjs.map +1 -1
  23. package/dist/bedrock-converse.d.cts.map +1 -1
  24. package/dist/bedrock-converse.d.ts.map +1 -1
  25. package/dist/bedrock-converse.js +37 -3
  26. package/dist/bedrock-converse.js.map +1 -1
  27. package/dist/bedrock.cjs +50 -13
  28. package/dist/bedrock.cjs.map +1 -1
  29. package/dist/bedrock.d.cts.map +1 -1
  30. package/dist/bedrock.d.ts.map +1 -1
  31. package/dist/bedrock.js +51 -14
  32. package/dist/bedrock.js.map +1 -1
  33. package/dist/byteplus-video.cjs +954 -0
  34. package/dist/byteplus-video.cjs.map +1 -0
  35. package/dist/byteplus-video.d.cts +176 -0
  36. package/dist/byteplus-video.d.cts.map +1 -0
  37. package/dist/byteplus-video.d.ts +176 -0
  38. package/dist/byteplus-video.d.ts.map +1 -0
  39. package/dist/byteplus-video.js +947 -0
  40. package/dist/byteplus-video.js.map +1 -0
  41. package/dist/chaos.cjs +36 -7
  42. package/dist/chaos.cjs.map +1 -1
  43. package/dist/chaos.d.cts +8 -3
  44. package/dist/chaos.d.cts.map +1 -1
  45. package/dist/chaos.d.ts +8 -3
  46. package/dist/chaos.d.ts.map +1 -1
  47. package/dist/chaos.js +36 -8
  48. package/dist/chaos.js.map +1 -1
  49. package/dist/cli.cjs +5 -1
  50. package/dist/cli.cjs.map +1 -1
  51. package/dist/cli.js +5 -1
  52. package/dist/cli.js.map +1 -1
  53. package/dist/cohere.cjs +58 -5
  54. package/dist/cohere.cjs.map +1 -1
  55. package/dist/cohere.d.cts.map +1 -1
  56. package/dist/cohere.d.ts.map +1 -1
  57. package/dist/cohere.js +59 -6
  58. package/dist/cohere.js.map +1 -1
  59. package/dist/config-loader.d.cts.map +1 -1
  60. package/dist/elevenlabs-audio.cjs +36 -2
  61. package/dist/elevenlabs-audio.cjs.map +1 -1
  62. package/dist/elevenlabs-audio.d.cts.map +1 -1
  63. package/dist/elevenlabs-audio.d.ts.map +1 -1
  64. package/dist/elevenlabs-audio.js +37 -3
  65. package/dist/elevenlabs-audio.js.map +1 -1
  66. package/dist/embeddings.cjs +58 -3
  67. package/dist/embeddings.cjs.map +1 -1
  68. package/dist/embeddings.d.cts.map +1 -1
  69. package/dist/embeddings.d.ts.map +1 -1
  70. package/dist/embeddings.js +59 -4
  71. package/dist/embeddings.js.map +1 -1
  72. package/dist/fal-audio.cjs +38 -2
  73. package/dist/fal-audio.cjs.map +1 -1
  74. package/dist/fal-audio.d.cts.map +1 -1
  75. package/dist/fal-audio.d.ts.map +1 -1
  76. package/dist/fal-audio.js +39 -3
  77. package/dist/fal-audio.js.map +1 -1
  78. package/dist/fixture-loader.cjs +0 -0
  79. package/dist/fixture-loader.cjs.map +1 -1
  80. package/dist/fixture-loader.d.cts +9 -1
  81. package/dist/fixture-loader.d.cts.map +1 -1
  82. package/dist/fixture-loader.d.ts +9 -1
  83. package/dist/fixture-loader.d.ts.map +1 -1
  84. package/dist/fixture-loader.js +0 -0
  85. package/dist/fixture-loader.js.map +1 -1
  86. package/dist/gemini-embeddings.cjs +19 -1
  87. package/dist/gemini-embeddings.cjs.map +1 -1
  88. package/dist/gemini-embeddings.js +20 -2
  89. package/dist/gemini-embeddings.js.map +1 -1
  90. package/dist/gemini-interactions.cjs +15 -1
  91. package/dist/gemini-interactions.cjs.map +1 -1
  92. package/dist/gemini-interactions.d.cts.map +1 -1
  93. package/dist/gemini-interactions.d.ts.map +1 -1
  94. package/dist/gemini-interactions.js +16 -2
  95. package/dist/gemini-interactions.js.map +1 -1
  96. package/dist/gemini.cjs +70 -6
  97. package/dist/gemini.cjs.map +1 -1
  98. package/dist/gemini.d.cts.map +1 -1
  99. package/dist/gemini.d.ts.map +1 -1
  100. package/dist/gemini.js +71 -7
  101. package/dist/gemini.js.map +1 -1
  102. package/dist/grok-video.cjs +2 -2
  103. package/dist/grok-video.cjs.map +1 -1
  104. package/dist/grok-video.d.cts.map +1 -1
  105. package/dist/grok-video.d.ts.map +1 -1
  106. package/dist/grok-video.js +2 -2
  107. package/dist/grok-video.js.map +1 -1
  108. package/dist/helpers.cjs +160 -5
  109. package/dist/helpers.cjs.map +1 -1
  110. package/dist/helpers.d.cts.map +1 -1
  111. package/dist/helpers.d.ts.map +1 -1
  112. package/dist/helpers.js +152 -6
  113. package/dist/helpers.js.map +1 -1
  114. package/dist/images.cjs +24 -6
  115. package/dist/images.cjs.map +1 -1
  116. package/dist/images.d.cts +11 -3
  117. package/dist/images.d.cts.map +1 -1
  118. package/dist/images.d.ts +11 -3
  119. package/dist/images.d.ts.map +1 -1
  120. package/dist/images.js +25 -7
  121. package/dist/images.js.map +1 -1
  122. package/dist/index.cjs +7 -0
  123. package/dist/index.d.cts +2 -1
  124. package/dist/index.d.ts +2 -1
  125. package/dist/index.js +2 -1
  126. package/dist/llmock.cjs +15 -0
  127. package/dist/llmock.cjs.map +1 -1
  128. package/dist/llmock.d.cts +12 -0
  129. package/dist/llmock.d.cts.map +1 -1
  130. package/dist/llmock.d.ts +12 -0
  131. package/dist/llmock.d.ts.map +1 -1
  132. package/dist/llmock.js +16 -1
  133. package/dist/llmock.js.map +1 -1
  134. package/dist/mcp-mock.cjs +11 -0
  135. package/dist/mcp-mock.cjs.map +1 -1
  136. package/dist/mcp-mock.d.cts.map +1 -1
  137. package/dist/mcp-mock.d.ts.map +1 -1
  138. package/dist/mcp-mock.js +11 -0
  139. package/dist/mcp-mock.js.map +1 -1
  140. package/dist/messages.cjs +18 -1
  141. package/dist/messages.cjs.map +1 -1
  142. package/dist/messages.d.cts.map +1 -1
  143. package/dist/messages.d.ts.map +1 -1
  144. package/dist/messages.js +19 -2
  145. package/dist/messages.js.map +1 -1
  146. package/dist/metrics.cjs +20 -0
  147. package/dist/metrics.cjs.map +1 -1
  148. package/dist/metrics.d.cts.map +1 -1
  149. package/dist/metrics.d.ts.map +1 -1
  150. package/dist/metrics.js +19 -1
  151. package/dist/metrics.js.map +1 -1
  152. package/dist/moderation.cjs +67 -3
  153. package/dist/moderation.cjs.map +1 -1
  154. package/dist/moderation.d.cts +1 -0
  155. package/dist/moderation.d.cts.map +1 -1
  156. package/dist/moderation.d.ts +1 -0
  157. package/dist/moderation.d.ts.map +1 -1
  158. package/dist/moderation.js +68 -4
  159. package/dist/moderation.js.map +1 -1
  160. package/dist/ollama.cjs +75 -5
  161. package/dist/ollama.cjs.map +1 -1
  162. package/dist/ollama.d.cts.map +1 -1
  163. package/dist/ollama.d.ts.map +1 -1
  164. package/dist/ollama.js +76 -6
  165. package/dist/ollama.js.map +1 -1
  166. package/dist/openrouter-video.cjs +4 -4
  167. package/dist/openrouter-video.cjs.map +1 -1
  168. package/dist/openrouter-video.d.cts.map +1 -1
  169. package/dist/openrouter-video.d.ts.map +1 -1
  170. package/dist/openrouter-video.js +4 -4
  171. package/dist/openrouter-video.js.map +1 -1
  172. package/dist/provider-auth.cjs +2 -0
  173. package/dist/provider-auth.cjs.map +1 -1
  174. package/dist/provider-auth.js +2 -0
  175. package/dist/provider-auth.js.map +1 -1
  176. package/dist/rerank.cjs +65 -2
  177. package/dist/rerank.cjs.map +1 -1
  178. package/dist/rerank.d.cts +1 -1
  179. package/dist/rerank.d.cts.map +1 -1
  180. package/dist/rerank.d.ts +1 -1
  181. package/dist/rerank.d.ts.map +1 -1
  182. package/dist/rerank.js +66 -3
  183. package/dist/rerank.js.map +1 -1
  184. package/dist/responses.cjs +18 -1
  185. package/dist/responses.cjs.map +1 -1
  186. package/dist/responses.d.cts.map +1 -1
  187. package/dist/responses.d.ts.map +1 -1
  188. package/dist/responses.js +19 -2
  189. package/dist/responses.js.map +1 -1
  190. package/dist/router.cjs +1 -0
  191. package/dist/router.cjs.map +1 -1
  192. package/dist/router.d.cts.map +1 -1
  193. package/dist/router.d.ts.map +1 -1
  194. package/dist/router.js +1 -0
  195. package/dist/router.js.map +1 -1
  196. package/dist/search.cjs +67 -3
  197. package/dist/search.cjs.map +1 -1
  198. package/dist/search.d.cts +1 -0
  199. package/dist/search.d.cts.map +1 -1
  200. package/dist/search.d.ts +1 -0
  201. package/dist/search.d.ts.map +1 -1
  202. package/dist/search.js +68 -4
  203. package/dist/search.js.map +1 -1
  204. package/dist/server.cjs +406 -17
  205. package/dist/server.cjs.map +1 -1
  206. package/dist/server.d.cts +2 -0
  207. package/dist/server.d.cts.map +1 -1
  208. package/dist/server.d.ts +2 -0
  209. package/dist/server.d.ts.map +1 -1
  210. package/dist/server.js +410 -21
  211. package/dist/server.js.map +1 -1
  212. package/dist/speech.cjs +18 -1
  213. package/dist/speech.cjs.map +1 -1
  214. package/dist/speech.d.cts.map +1 -1
  215. package/dist/speech.d.ts.map +1 -1
  216. package/dist/speech.js +19 -2
  217. package/dist/speech.js.map +1 -1
  218. package/dist/stream-collapse.cjs +2 -1
  219. package/dist/stream-collapse.cjs.map +1 -1
  220. package/dist/stream-collapse.js +2 -1
  221. package/dist/stream-collapse.js.map +1 -1
  222. package/dist/transcription.cjs +21 -2
  223. package/dist/transcription.cjs.map +1 -1
  224. package/dist/transcription.d.cts.map +1 -1
  225. package/dist/transcription.d.ts.map +1 -1
  226. package/dist/transcription.js +21 -2
  227. package/dist/transcription.js.map +1 -1
  228. package/dist/types.d.cts +58 -5
  229. package/dist/types.d.cts.map +1 -1
  230. package/dist/types.d.ts +58 -5
  231. package/dist/types.d.ts.map +1 -1
  232. package/dist/vector-mock.cjs +88 -0
  233. package/dist/vector-mock.cjs.map +1 -1
  234. package/dist/vector-mock.d.cts.map +1 -1
  235. package/dist/vector-mock.d.ts.map +1 -1
  236. package/dist/vector-mock.js +89 -1
  237. package/dist/vector-mock.js.map +1 -1
  238. package/dist/vector-types.d.cts.map +1 -1
  239. package/dist/veo-video.cjs +2 -2
  240. package/dist/veo-video.cjs.map +1 -1
  241. package/dist/veo-video.d.cts.map +1 -1
  242. package/dist/veo-video.d.ts.map +1 -1
  243. package/dist/veo-video.js +2 -2
  244. package/dist/veo-video.js.map +1 -1
  245. package/dist/video.cjs +19 -2
  246. package/dist/video.cjs.map +1 -1
  247. package/dist/video.d.cts.map +1 -1
  248. package/dist/video.d.ts.map +1 -1
  249. package/dist/video.js +20 -3
  250. package/dist/video.js.map +1 -1
  251. package/package.json +2 -2
@@ -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\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 if (response.status !== undefined && (response.status < 100 || response.status > 599)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `error status ${response.status} is not a valid HTTP status code`,\n });\n }\n // `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 { 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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"}
@@ -31,6 +31,24 @@ async function handleGeminiEmbedContent(req, res, raw, model, fixtures, journal,
31
31
  } }));
32
32
  return;
33
33
  }
34
+ if (!require_helpers.isJsonObject(embedReq)) {
35
+ journal.add({
36
+ method: req.method ?? "POST",
37
+ path: req.url ?? `/v1beta/models/${model}:embedContent`,
38
+ headers: require_helpers.flattenHeaders(req.headers),
39
+ body: null,
40
+ response: {
41
+ status: 400,
42
+ fixture: null
43
+ }
44
+ });
45
+ require_sse_writer.writeErrorResponse(res, 400, JSON.stringify({ error: {
46
+ message: "Request body must be a JSON object",
47
+ code: 400,
48
+ status: "INVALID_ARGUMENT"
49
+ } }));
50
+ return;
51
+ }
34
52
  const inputText = (embedReq.content?.parts ?? []).filter((p) => p.text !== void 0).map((p) => p.text).join(" ");
35
53
  const syntheticReq = {
36
54
  model: embedReq.model ?? model,
@@ -46,7 +64,7 @@ async function handleGeminiEmbedContent(req, res, raw, model, fixtures, journal,
46
64
  journal.incrementFixtureMatchCount(fixture, fixtures, testId);
47
65
  logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);
48
66
  } else logger.debug(`No fixture matched for request`);
49
- if (require_chaos.applyChaos(res, fixture, defaults.chaos, req.headers, journal, {
67
+ if (require_chaos.applyChaos(res, fixture, defaults.chaos, req.headers, req.url, journal, {
50
68
  method: req.method ?? "POST",
51
69
  path,
52
70
  headers: require_helpers.flattenHeaders(req.headers),
@@ -1 +1 @@
1
- {"version":3,"file":"gemini-embeddings.cjs","names":["flattenHeaders","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 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 // 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 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","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,4 +1,4 @@
1
- import { flattenHeaders, generateDeterministicEmbedding, getContext, getTestId, isEmbeddingResponse, isErrorResponse, resolveResponse, resolveStrictMode, strictNoMatchLogLine, strictNoMatchMessage, strictOverrideField } from "./helpers.js";
1
+ import { flattenHeaders, generateDeterministicEmbedding, getContext, getTestId, isEmbeddingResponse, isErrorResponse, isJsonObject, resolveResponse, resolveStrictMode, strictNoMatchLogLine, strictNoMatchMessage, strictOverrideField } from "./helpers.js";
2
2
  import { matchFixtureDiagnostic } from "./router.js";
3
3
  import { writeErrorResponse } from "./sse-writer.js";
4
4
  import { applyChaos } from "./chaos.js";
@@ -31,6 +31,24 @@ async function handleGeminiEmbedContent(req, res, raw, model, fixtures, journal,
31
31
  } }));
32
32
  return;
33
33
  }
34
+ if (!isJsonObject(embedReq)) {
35
+ journal.add({
36
+ method: req.method ?? "POST",
37
+ path: req.url ?? `/v1beta/models/${model}:embedContent`,
38
+ headers: flattenHeaders(req.headers),
39
+ body: null,
40
+ response: {
41
+ status: 400,
42
+ fixture: null
43
+ }
44
+ });
45
+ writeErrorResponse(res, 400, JSON.stringify({ error: {
46
+ message: "Request body must be a JSON object",
47
+ code: 400,
48
+ status: "INVALID_ARGUMENT"
49
+ } }));
50
+ return;
51
+ }
34
52
  const inputText = (embedReq.content?.parts ?? []).filter((p) => p.text !== void 0).map((p) => p.text).join(" ");
35
53
  const syntheticReq = {
36
54
  model: embedReq.model ?? model,
@@ -46,7 +64,7 @@ async function handleGeminiEmbedContent(req, res, raw, model, fixtures, journal,
46
64
  journal.incrementFixtureMatchCount(fixture, fixtures, testId);
47
65
  logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);
48
66
  } else logger.debug(`No fixture matched for request`);
49
- if (applyChaos(res, fixture, defaults.chaos, req.headers, journal, {
67
+ if (applyChaos(res, fixture, defaults.chaos, req.headers, req.url, journal, {
50
68
  method: req.method ?? "POST",
51
69
  path,
52
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 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 // 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 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 { 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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"}
@@ -532,6 +532,20 @@ async function handleGeminiInteractions(req, res, raw, fixtures, journal, defaul
532
532
  require_sse_writer.writeErrorResponse(res, 400, JSON.stringify(buildInteractionsErrorResponse(`Malformed JSON body: ${detail}`, "INVALID_ARGUMENT")));
533
533
  return;
534
534
  }
535
+ if (!require_helpers.isJsonObject(interactionsReq)) {
536
+ journal.add({
537
+ method: req.method ?? "POST",
538
+ path: urlPath,
539
+ headers: require_helpers.flattenHeaders(req.headers),
540
+ body: null,
541
+ response: {
542
+ status: 400,
543
+ fixture: null
544
+ }
545
+ });
546
+ require_sse_writer.writeErrorResponse(res, 400, JSON.stringify(buildInteractionsErrorResponse("Request body must be a JSON object", "INVALID_ARGUMENT")));
547
+ return;
548
+ }
535
549
  const completionReq = geminiInteractionsToCompletionRequest(interactionsReq);
536
550
  completionReq._endpointType = "chat";
537
551
  completionReq._context = require_helpers.getContext(req);
@@ -540,7 +554,7 @@ async function handleGeminiInteractions(req, res, raw, fixtures, journal, defaul
540
554
  const testId = require_helpers.getTestId(req);
541
555
  const { fixture, skippedBySequenceOrTurn } = require_router.matchFixtureDiagnostic(fixtures, completionReq, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform);
542
556
  if (fixture) journal.incrementFixtureMatchCount(fixture, fixtures, testId);
543
- if (require_chaos.applyChaos(res, fixture, defaults.chaos, req.headers, journal, {
557
+ if (require_chaos.applyChaos(res, fixture, defaults.chaos, req.headers, req.url, journal, {
544
558
  method: req.method ?? "POST",
545
559
  path: urlPath,
546
560
  headers: require_helpers.flattenHeaders(req.headers),