@copilotkit/aimock 1.20.0 → 1.22.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 (208) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +47 -0
  3. package/README.md +1 -0
  4. package/dist/a2a-mock.cjs +1 -1
  5. package/dist/a2a-mock.cjs.map +1 -1
  6. package/dist/a2a-mock.d.cts.map +1 -1
  7. package/dist/a2a-mock.d.ts.map +1 -1
  8. package/dist/a2a-mock.js +1 -1
  9. package/dist/a2a-mock.js.map +1 -1
  10. package/dist/agui-recorder.cjs +25 -12
  11. package/dist/agui-recorder.cjs.map +1 -1
  12. package/dist/agui-recorder.js +25 -12
  13. package/dist/agui-recorder.js.map +1 -1
  14. package/dist/agui-types.d.cts.map +1 -1
  15. package/dist/bedrock-converse.cjs +18 -12
  16. package/dist/bedrock-converse.cjs.map +1 -1
  17. package/dist/bedrock-converse.d.cts.map +1 -1
  18. package/dist/bedrock-converse.d.ts.map +1 -1
  19. package/dist/bedrock-converse.js +19 -13
  20. package/dist/bedrock-converse.js.map +1 -1
  21. package/dist/bedrock.cjs +18 -12
  22. package/dist/bedrock.cjs.map +1 -1
  23. package/dist/bedrock.d.cts.map +1 -1
  24. package/dist/bedrock.d.ts.map +1 -1
  25. package/dist/bedrock.js +19 -13
  26. package/dist/bedrock.js.map +1 -1
  27. package/dist/cli.cjs +1 -1
  28. package/dist/cli.cjs.map +1 -1
  29. package/dist/cli.js +1 -1
  30. package/dist/cli.js.map +1 -1
  31. package/dist/cohere.cjs +9 -6
  32. package/dist/cohere.cjs.map +1 -1
  33. package/dist/cohere.d.cts.map +1 -1
  34. package/dist/cohere.d.ts.map +1 -1
  35. package/dist/cohere.js +10 -7
  36. package/dist/cohere.js.map +1 -1
  37. package/dist/config-loader.d.cts.map +1 -1
  38. package/dist/elevenlabs-audio.cjs +8 -5
  39. package/dist/elevenlabs-audio.cjs.map +1 -1
  40. package/dist/elevenlabs-audio.d.cts.map +1 -1
  41. package/dist/elevenlabs-audio.d.ts.map +1 -1
  42. package/dist/elevenlabs-audio.js +9 -6
  43. package/dist/elevenlabs-audio.js.map +1 -1
  44. package/dist/embeddings.cjs +6 -4
  45. package/dist/embeddings.cjs.map +1 -1
  46. package/dist/embeddings.d.cts.map +1 -1
  47. package/dist/embeddings.d.ts.map +1 -1
  48. package/dist/embeddings.js +7 -5
  49. package/dist/embeddings.js.map +1 -1
  50. package/dist/fal-audio.cjs +16 -10
  51. package/dist/fal-audio.cjs.map +1 -1
  52. package/dist/fal-audio.d.cts.map +1 -1
  53. package/dist/fal-audio.d.ts.map +1 -1
  54. package/dist/fal-audio.js +17 -11
  55. package/dist/fal-audio.js.map +1 -1
  56. package/dist/fal.cjs +5 -3
  57. package/dist/fal.cjs.map +1 -1
  58. package/dist/fal.d.cts.map +1 -1
  59. package/dist/fal.d.ts.map +1 -1
  60. package/dist/fal.js +6 -4
  61. package/dist/fal.js.map +1 -1
  62. package/dist/fixture-loader.cjs +19 -5
  63. package/dist/fixture-loader.cjs.map +1 -1
  64. package/dist/fixture-loader.js +19 -5
  65. package/dist/fixture-loader.js.map +1 -1
  66. package/dist/gemini-interactions.cjs +10 -7
  67. package/dist/gemini-interactions.cjs.map +1 -1
  68. package/dist/gemini-interactions.d.cts.map +1 -1
  69. package/dist/gemini-interactions.d.ts.map +1 -1
  70. package/dist/gemini-interactions.js +11 -8
  71. package/dist/gemini-interactions.js.map +1 -1
  72. package/dist/gemini.cjs +10 -7
  73. package/dist/gemini.cjs.map +1 -1
  74. package/dist/gemini.d.cts.map +1 -1
  75. package/dist/gemini.d.ts.map +1 -1
  76. package/dist/gemini.js +11 -8
  77. package/dist/gemini.js.map +1 -1
  78. package/dist/helpers.cjs +31 -0
  79. package/dist/helpers.cjs.map +1 -1
  80. package/dist/helpers.d.cts +1 -0
  81. package/dist/helpers.d.cts.map +1 -1
  82. package/dist/helpers.d.ts +1 -0
  83. package/dist/helpers.d.ts.map +1 -1
  84. package/dist/helpers.js +30 -1
  85. package/dist/helpers.js.map +1 -1
  86. package/dist/images.cjs +8 -5
  87. package/dist/images.cjs.map +1 -1
  88. package/dist/images.d.cts.map +1 -1
  89. package/dist/images.d.ts.map +1 -1
  90. package/dist/images.js +9 -6
  91. package/dist/images.js.map +1 -1
  92. package/dist/mcp-mock.cjs +1 -1
  93. package/dist/mcp-mock.cjs.map +1 -1
  94. package/dist/mcp-mock.d.cts.map +1 -1
  95. package/dist/mcp-mock.d.ts.map +1 -1
  96. package/dist/mcp-mock.js +1 -1
  97. package/dist/mcp-mock.js.map +1 -1
  98. package/dist/messages.cjs +9 -6
  99. package/dist/messages.cjs.map +1 -1
  100. package/dist/messages.d.cts.map +1 -1
  101. package/dist/messages.d.ts.map +1 -1
  102. package/dist/messages.js +10 -7
  103. package/dist/messages.js.map +1 -1
  104. package/dist/moderation.cjs +3 -2
  105. package/dist/moderation.cjs.map +1 -1
  106. package/dist/moderation.js +3 -2
  107. package/dist/moderation.js.map +1 -1
  108. package/dist/ollama.cjs +18 -12
  109. package/dist/ollama.cjs.map +1 -1
  110. package/dist/ollama.d.cts.map +1 -1
  111. package/dist/ollama.d.ts.map +1 -1
  112. package/dist/ollama.js +19 -13
  113. package/dist/ollama.js.map +1 -1
  114. package/dist/recorder.cjs +82 -38
  115. package/dist/recorder.cjs.map +1 -1
  116. package/dist/recorder.d.cts +3 -2
  117. package/dist/recorder.d.cts.map +1 -1
  118. package/dist/recorder.d.ts +3 -2
  119. package/dist/recorder.d.ts.map +1 -1
  120. package/dist/recorder.js +82 -38
  121. package/dist/recorder.js.map +1 -1
  122. package/dist/rerank.cjs +3 -2
  123. package/dist/rerank.cjs.map +1 -1
  124. package/dist/rerank.js +3 -2
  125. package/dist/rerank.js.map +1 -1
  126. package/dist/responses.cjs +9 -6
  127. package/dist/responses.cjs.map +1 -1
  128. package/dist/responses.d.cts.map +1 -1
  129. package/dist/responses.d.ts.map +1 -1
  130. package/dist/responses.js +10 -7
  131. package/dist/responses.js.map +1 -1
  132. package/dist/router.cjs +18 -5
  133. package/dist/router.cjs.map +1 -1
  134. package/dist/router.js +18 -5
  135. package/dist/router.js.map +1 -1
  136. package/dist/search.cjs +3 -2
  137. package/dist/search.cjs.map +1 -1
  138. package/dist/search.js +3 -2
  139. package/dist/search.js.map +1 -1
  140. package/dist/server.cjs +135 -73
  141. package/dist/server.cjs.map +1 -1
  142. package/dist/server.d.cts.map +1 -1
  143. package/dist/server.d.ts.map +1 -1
  144. package/dist/server.js +136 -74
  145. package/dist/server.js.map +1 -1
  146. package/dist/speech.cjs +8 -5
  147. package/dist/speech.cjs.map +1 -1
  148. package/dist/speech.d.cts.map +1 -1
  149. package/dist/speech.d.ts.map +1 -1
  150. package/dist/speech.js +9 -6
  151. package/dist/speech.js.map +1 -1
  152. package/dist/stream-collapse.cjs +51 -21
  153. package/dist/stream-collapse.cjs.map +1 -1
  154. package/dist/stream-collapse.d.cts +1 -0
  155. package/dist/stream-collapse.d.cts.map +1 -1
  156. package/dist/stream-collapse.d.ts +1 -0
  157. package/dist/stream-collapse.d.ts.map +1 -1
  158. package/dist/stream-collapse.js +51 -21
  159. package/dist/stream-collapse.js.map +1 -1
  160. package/dist/transcription.cjs +5 -3
  161. package/dist/transcription.cjs.map +1 -1
  162. package/dist/transcription.d.cts.map +1 -1
  163. package/dist/transcription.d.ts.map +1 -1
  164. package/dist/transcription.js +6 -4
  165. package/dist/transcription.js.map +1 -1
  166. package/dist/types.d.cts +21 -9
  167. package/dist/types.d.cts.map +1 -1
  168. package/dist/types.d.ts +21 -9
  169. package/dist/types.d.ts.map +1 -1
  170. package/dist/vector-mock.cjs +10 -8
  171. package/dist/vector-mock.cjs.map +1 -1
  172. package/dist/vector-mock.d.cts.map +1 -1
  173. package/dist/vector-mock.d.ts.map +1 -1
  174. package/dist/vector-mock.js +10 -8
  175. package/dist/vector-mock.js.map +1 -1
  176. package/dist/vector-types.d.cts.map +1 -1
  177. package/dist/video.cjs +8 -5
  178. package/dist/video.cjs.map +1 -1
  179. package/dist/video.d.cts.map +1 -1
  180. package/dist/video.d.ts.map +1 -1
  181. package/dist/video.js +9 -6
  182. package/dist/video.js.map +1 -1
  183. package/dist/ws-gemini-live.cjs +6 -4
  184. package/dist/ws-gemini-live.cjs.map +1 -1
  185. package/dist/ws-gemini-live.d.cts +2 -0
  186. package/dist/ws-gemini-live.d.cts.map +1 -1
  187. package/dist/ws-gemini-live.d.ts +2 -0
  188. package/dist/ws-gemini-live.d.ts.map +1 -1
  189. package/dist/ws-gemini-live.js +7 -5
  190. package/dist/ws-gemini-live.js.map +1 -1
  191. package/dist/ws-realtime.cjs +6 -4
  192. package/dist/ws-realtime.cjs.map +1 -1
  193. package/dist/ws-realtime.d.cts +2 -0
  194. package/dist/ws-realtime.d.cts.map +1 -1
  195. package/dist/ws-realtime.d.ts +2 -0
  196. package/dist/ws-realtime.d.ts.map +1 -1
  197. package/dist/ws-realtime.js +7 -5
  198. package/dist/ws-realtime.js.map +1 -1
  199. package/dist/ws-responses.cjs +6 -4
  200. package/dist/ws-responses.cjs.map +1 -1
  201. package/dist/ws-responses.d.cts +2 -0
  202. package/dist/ws-responses.d.cts.map +1 -1
  203. package/dist/ws-responses.d.ts +2 -0
  204. package/dist/ws-responses.d.ts.map +1 -1
  205. package/dist/ws-responses.js +7 -5
  206. package/dist/ws-responses.js.map +1 -1
  207. package/package.json +1 -1
  208. package/skills/write-fixtures/SKILL.md +1 -0
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.js","names":[],"sources":["../src/helpers.ts"],"sourcesContent":["import { createHash, randomBytes } from \"node:crypto\";\nimport type * as http from \"node:http\";\nimport type {\n ChatCompletionRequest,\n Fixture,\n FixtureResponse,\n ResponseFactory,\n TextResponse,\n ToolCallResponse,\n ContentWithToolCallsResponse,\n ErrorResponse,\n EmbeddingResponse,\n ImageResponse,\n AudioResponse,\n TranscriptionResponse,\n VideoResponse,\n RawJSONResponse,\n SSEChunk,\n ToolCall,\n ChatCompletion,\n ResponseOverrides,\n} from \"./types.js\";\n\nconst REDACTED_HEADERS = new Set([\"authorization\", \"x-api-key\", \"api-key\"]);\n\nexport function flattenHeaders(headers: http.IncomingHttpHeaders): Record<string, string> {\n const flat: Record<string, string> = {};\n for (const [key, value] of Object.entries(headers)) {\n if (value === undefined) continue;\n if (REDACTED_HEADERS.has(key.toLowerCase())) {\n flat[key] = \"[REDACTED]\";\n } else {\n flat[key] = Array.isArray(value) ? value.join(\", \") : value;\n }\n }\n return flat;\n}\n\nexport function isResponseFactory(r: FixtureResponse | ResponseFactory): r is ResponseFactory {\n return typeof r === \"function\";\n}\n\nexport async function resolveResponse(\n fixture: Fixture,\n request: ChatCompletionRequest,\n): Promise<FixtureResponse> {\n if (typeof fixture.response === \"function\") {\n const raw = await fixture.response(request);\n return normalizeFactoryResponse(raw);\n }\n return fixture.response;\n}\n\nfunction normalizeFactoryResponse(raw: FixtureResponse): FixtureResponse {\n const r = { ...raw } as Record<string, unknown>;\n if (typeof r.content === \"object\" && r.content !== null) {\n r.content = JSON.stringify(r.content);\n }\n if (Array.isArray(r.toolCalls)) {\n r.toolCalls = (r.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 return r as unknown as FixtureResponse;\n}\n\nexport function generateId(prefix = \"chatcmpl\"): string {\n return `${prefix}-${randomBytes(12).toString(\"base64url\")}`;\n}\n\nexport function generateToolCallId(): string {\n return `call_${randomBytes(12).toString(\"base64url\")}`;\n}\n\nexport function generateMessageId(): string {\n return `msg_${randomBytes(12).toString(\"base64url\")}`;\n}\n\nexport function generateToolUseId(): string {\n return `toolu_${randomBytes(12).toString(\"base64url\")}`;\n}\n\nexport function isTextResponse(r: FixtureResponse): r is TextResponse {\n return \"content\" in r && typeof (r as TextResponse).content === \"string\" && !(\"toolCalls\" in r);\n}\n\nexport function isToolCallResponse(r: FixtureResponse): r is ToolCallResponse {\n return (\n \"toolCalls\" in r &&\n Array.isArray((r as ToolCallResponse).toolCalls) &&\n !(\"content\" in r && typeof (r as unknown as Record<string, unknown>).content === \"string\")\n );\n}\n\nexport function isContentWithToolCallsResponse(\n r: FixtureResponse,\n): r is ContentWithToolCallsResponse {\n return (\n \"content\" in r &&\n typeof (r as ContentWithToolCallsResponse).content === \"string\" &&\n \"toolCalls\" in r &&\n Array.isArray((r as ContentWithToolCallsResponse).toolCalls)\n );\n}\n\nexport function isErrorResponse(r: FixtureResponse): r is ErrorResponse {\n return (\n \"error\" in r &&\n (r as ErrorResponse).error !== null &&\n typeof (r as ErrorResponse).error === \"object\"\n );\n}\n\nexport function isEmbeddingResponse(r: FixtureResponse): r is EmbeddingResponse {\n return \"embedding\" in r && Array.isArray((r as EmbeddingResponse).embedding);\n}\n\nexport function isImageResponse(r: FixtureResponse): r is ImageResponse {\n return (\n (\"image\" in r && r.image != null) ||\n (\"images\" in r && Array.isArray((r as ImageResponse).images))\n );\n}\n\nexport function isAudioResponse(r: FixtureResponse): r is AudioResponse {\n if (!(\"audio\" in r)) return false;\n const a = (r as AudioResponse).audio;\n return typeof a === \"string\" || (typeof a === \"object\" && a !== null && \"b64Json\" in a);\n}\n\n/**\n * Map audio format shorthand to MIME content types.\n * Shared between speech, ElevenLabs, and fal audio handlers.\n */\nexport const FORMAT_TO_CONTENT_TYPE: Record<string, string> = {\n mp3: \"audio/mpeg\",\n opus: \"audio/opus\",\n aac: \"audio/aac\",\n flac: \"audio/flac\",\n wav: \"audio/wav\",\n pcm: \"audio/pcm\",\n};\n\n/**\n * Resolve a format string (e.g. \"mp3\", \"opus\") to its MIME content type.\n * Falls back to \"application/octet-stream\" for unknown formats.\n */\nexport function formatToMime(format: string): string {\n return FORMAT_TO_CONTENT_TYPE[format] ?? \"application/octet-stream\";\n}\n\nexport function isTranscriptionResponse(r: FixtureResponse): r is TranscriptionResponse {\n return (\n \"transcription\" in r &&\n (r as TranscriptionResponse).transcription != null &&\n typeof (r as TranscriptionResponse).transcription === \"object\"\n );\n}\n\nexport function isVideoResponse(r: FixtureResponse): r is VideoResponse {\n return (\n \"video\" in r &&\n (r as VideoResponse).video != null &&\n typeof (r as VideoResponse).video === \"object\"\n );\n}\n\nexport function isJSONResponse(r: FixtureResponse): r is RawJSONResponse {\n return \"json\" in r && (r as RawJSONResponse).json !== undefined;\n}\n\nexport function extractOverrides(\n response: TextResponse | ToolCallResponse | ContentWithToolCallsResponse,\n): ResponseOverrides {\n const r = response;\n return {\n ...(r.id !== undefined && { id: r.id }),\n ...(r.created !== undefined && { created: r.created }),\n ...(r.model !== undefined && { model: r.model }),\n ...(r.usage !== undefined && { usage: r.usage }),\n ...(r.systemFingerprint !== undefined && { systemFingerprint: r.systemFingerprint }),\n ...(r.finishReason !== undefined && { finishReason: r.finishReason }),\n ...(r.role !== undefined && { role: r.role }),\n };\n}\n\nexport function buildTextChunks(\n content: string,\n model: string,\n chunkSize: number,\n reasoning?: string,\n overrides?: ResponseOverrides,\n): SSEChunk[] {\n const id = overrides?.id ?? generateId();\n const created = overrides?.created ?? Math.floor(Date.now() / 1000);\n const effectiveModel = overrides?.model ?? model;\n const chunks: SSEChunk[] = [];\n const fingerprint = overrides?.systemFingerprint;\n\n // Reasoning chunks (emitted before content, OpenRouter format)\n if (reasoning) {\n for (let i = 0; i < reasoning.length; i += chunkSize) {\n const slice = reasoning.slice(i, i + chunkSize);\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n { index: 0, delta: { reasoning_content: slice }, logprobs: null, finish_reason: null },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n }\n }\n\n // Role chunk\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: { role: overrides?.role ?? \"assistant\", content: \"\" },\n logprobs: null,\n finish_reason: null,\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n // Content chunks\n for (let i = 0; i < content.length; i += chunkSize) {\n const slice = content.slice(i, i + chunkSize);\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [{ index: 0, delta: { content: slice }, logprobs: null, finish_reason: null }],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n }\n\n // Finish chunk\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n { index: 0, delta: {}, logprobs: null, finish_reason: overrides?.finishReason ?? \"stop\" },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n return chunks;\n}\n\nexport function buildToolCallChunks(\n toolCalls: ToolCall[],\n model: string,\n chunkSize: number,\n overrides?: ResponseOverrides,\n): SSEChunk[] {\n const id = overrides?.id ?? generateId();\n const created = overrides?.created ?? Math.floor(Date.now() / 1000);\n const effectiveModel = overrides?.model ?? model;\n const chunks: SSEChunk[] = [];\n const fingerprint = overrides?.systemFingerprint;\n\n // Role chunk\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: { role: overrides?.role ?? \"assistant\", content: null },\n logprobs: null,\n finish_reason: null,\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n // Tool call chunks — one initial chunk per tool call, then argument chunks\n for (let tcIdx = 0; tcIdx < toolCalls.length; tcIdx++) {\n const tc = toolCalls[tcIdx];\n const tcId = tc.id || generateToolCallId();\n\n // Initial tool call chunk (id + function name)\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: {\n tool_calls: [\n {\n index: tcIdx,\n id: tcId,\n type: \"function\",\n function: { name: tc.name, arguments: \"\" },\n },\n ],\n },\n logprobs: null,\n finish_reason: null,\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n // Argument streaming chunks\n const args = tc.arguments;\n for (let i = 0; i < args.length; i += chunkSize) {\n const slice = args.slice(i, i + chunkSize);\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: {\n tool_calls: [{ index: tcIdx, function: { arguments: slice } }],\n },\n logprobs: null,\n finish_reason: null,\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n }\n }\n\n // Finish chunk\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: {},\n logprobs: null,\n finish_reason: overrides?.finishReason ?? \"tool_calls\",\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n return chunks;\n}\n\n// Non-streaming response builders\n\nexport function buildTextCompletion(\n content: string,\n model: string,\n reasoning?: string,\n overrides?: ResponseOverrides,\n): ChatCompletion {\n const defaultUsage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };\n return {\n id: overrides?.id ?? generateId(),\n object: \"chat.completion\",\n created: overrides?.created ?? Math.floor(Date.now() / 1000),\n model: overrides?.model ?? model,\n choices: [\n {\n index: 0,\n message: {\n role: overrides?.role ?? \"assistant\",\n content,\n refusal: null,\n ...(reasoning ? { reasoning_content: reasoning } : {}),\n },\n logprobs: null,\n finish_reason: overrides?.finishReason ?? \"stop\",\n },\n ],\n usage: overrides?.usage\n ? {\n prompt_tokens: overrides.usage.prompt_tokens ?? defaultUsage.prompt_tokens,\n completion_tokens: overrides.usage.completion_tokens ?? defaultUsage.completion_tokens,\n total_tokens:\n overrides.usage.total_tokens ??\n (overrides.usage.prompt_tokens ?? 0) + (overrides.usage.completion_tokens ?? 0),\n }\n : defaultUsage,\n ...(overrides?.systemFingerprint !== undefined && {\n system_fingerprint: overrides.systemFingerprint,\n }),\n };\n}\n\nexport function buildToolCallCompletion(\n toolCalls: ToolCall[],\n model: string,\n overrides?: ResponseOverrides,\n): ChatCompletion {\n const defaultUsage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };\n return {\n id: overrides?.id ?? generateId(),\n object: \"chat.completion\",\n created: overrides?.created ?? Math.floor(Date.now() / 1000),\n model: overrides?.model ?? model,\n choices: [\n {\n index: 0,\n message: {\n role: overrides?.role ?? \"assistant\",\n content: null,\n refusal: null,\n tool_calls: toolCalls.map((tc) => ({\n id: tc.id || generateToolCallId(),\n type: \"function\" as const,\n function: { name: tc.name, arguments: tc.arguments },\n })),\n },\n logprobs: null,\n finish_reason: overrides?.finishReason ?? \"tool_calls\",\n },\n ],\n usage: overrides?.usage\n ? {\n prompt_tokens: overrides.usage.prompt_tokens ?? defaultUsage.prompt_tokens,\n completion_tokens: overrides.usage.completion_tokens ?? defaultUsage.completion_tokens,\n total_tokens:\n overrides.usage.total_tokens ??\n (overrides.usage.prompt_tokens ?? 0) + (overrides.usage.completion_tokens ?? 0),\n }\n : defaultUsage,\n ...(overrides?.systemFingerprint !== undefined && {\n system_fingerprint: overrides.systemFingerprint,\n }),\n };\n}\n\nexport function buildContentWithToolCallsChunks(\n content: string,\n toolCalls: ToolCall[],\n model: string,\n chunkSize: number,\n reasoning?: string,\n overrides?: ResponseOverrides,\n): SSEChunk[] {\n const id = overrides?.id ?? generateId();\n const created = overrides?.created ?? Math.floor(Date.now() / 1000);\n const effectiveModel = overrides?.model ?? model;\n const chunks: SSEChunk[] = [];\n const fingerprint = overrides?.systemFingerprint;\n\n // Reasoning chunks (emitted before content, OpenRouter format)\n if (reasoning) {\n for (let i = 0; i < reasoning.length; i += chunkSize) {\n const slice = reasoning.slice(i, i + chunkSize);\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n { index: 0, delta: { reasoning_content: slice }, logprobs: null, finish_reason: null },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n }\n }\n\n // Role chunk\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: { role: overrides?.role ?? \"assistant\", content: \"\" },\n logprobs: null,\n finish_reason: null,\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n // Content chunks\n for (let i = 0; i < content.length; i += chunkSize) {\n const slice = content.slice(i, i + chunkSize);\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [{ index: 0, delta: { content: slice }, logprobs: null, finish_reason: null }],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n }\n\n // Tool call chunks — one initial chunk per tool call, then argument chunks\n for (let tcIdx = 0; tcIdx < toolCalls.length; tcIdx++) {\n const tc = toolCalls[tcIdx];\n const tcId = tc.id || generateToolCallId();\n\n // Initial tool call chunk (id + function name)\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: {\n tool_calls: [\n {\n index: tcIdx,\n id: tcId,\n type: \"function\",\n function: { name: tc.name, arguments: \"\" },\n },\n ],\n },\n logprobs: null,\n finish_reason: null,\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n // Argument streaming chunks\n const args = tc.arguments;\n for (let i = 0; i < args.length; i += chunkSize) {\n const slice = args.slice(i, i + chunkSize);\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: {\n tool_calls: [{ index: tcIdx, function: { arguments: slice } }],\n },\n logprobs: null,\n finish_reason: null,\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n }\n }\n\n // Finish chunk\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: {},\n logprobs: null,\n finish_reason: overrides?.finishReason ?? \"tool_calls\",\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n return chunks;\n}\n\nexport function buildContentWithToolCallsCompletion(\n content: string,\n toolCalls: ToolCall[],\n model: string,\n reasoning?: string,\n overrides?: ResponseOverrides,\n): ChatCompletion {\n const defaultUsage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };\n return {\n id: overrides?.id ?? generateId(),\n object: \"chat.completion\",\n created: overrides?.created ?? Math.floor(Date.now() / 1000),\n model: overrides?.model ?? model,\n choices: [\n {\n index: 0,\n message: {\n role: overrides?.role ?? \"assistant\",\n content,\n refusal: null,\n ...(reasoning ? { reasoning_content: reasoning } : {}),\n tool_calls: toolCalls.map((tc) => ({\n id: tc.id || generateToolCallId(),\n type: \"function\" as const,\n function: { name: tc.name, arguments: tc.arguments },\n })),\n },\n logprobs: null,\n finish_reason: overrides?.finishReason ?? \"tool_calls\",\n },\n ],\n usage: overrides?.usage\n ? {\n prompt_tokens: overrides.usage.prompt_tokens ?? defaultUsage.prompt_tokens,\n completion_tokens: overrides.usage.completion_tokens ?? defaultUsage.completion_tokens,\n total_tokens:\n overrides.usage.total_tokens ??\n (overrides.usage.prompt_tokens ?? 0) + (overrides.usage.completion_tokens ?? 0),\n }\n : defaultUsage,\n ...(overrides?.systemFingerprint !== undefined && {\n system_fingerprint: overrides.systemFingerprint,\n }),\n };\n}\n\n// ─── HTTP helpers ─────────────────────────────────────────────────────────\n\nexport function readBody(req: http.IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => resolve(Buffer.concat(chunks).toString()));\n req.on(\"error\", reject);\n });\n}\n\n// ─── Pattern matching ─────────────────────────────────────────────────────\n\n/**\n * Case-insensitive substring/regex match used for search, rerank, and\n * moderation endpoints where exact casing rarely matters. String patterns\n * are lowercased on both sides before comparison.\n *\n * Note: This intentionally differs from the case-sensitive matching in\n * {@link matchFixture} (router.ts), where fixture authors expect exact\n * string matching against chat completion user messages.\n */\nexport function matchesPattern(text: string, pattern: string | RegExp): boolean {\n if (typeof pattern === \"string\") {\n return text.toLowerCase().includes(pattern.toLowerCase());\n }\n return pattern.test(text);\n}\n\nexport function getTestId(req: http.IncomingMessage): string {\n const headerValue = req.headers[\"x-test-id\"];\n if (Array.isArray(headerValue)) {\n if (headerValue.length > 0 && headerValue[0]) return headerValue[0];\n } else if (typeof headerValue === \"string\" && headerValue) {\n return headerValue;\n }\n\n const url = req.url ?? \"/\";\n const qIdx = url.indexOf(\"?\");\n if (qIdx !== -1) {\n const params = new URLSearchParams(url.slice(qIdx + 1));\n const queryValue = params.get(\"testId\");\n if (queryValue) return queryValue;\n }\n\n // Duplicated from journal.ts DEFAULT_TEST_ID — importing it here would create\n // a circular dependency (journal.ts imports from helpers.ts).\n return \"__default__\";\n}\n\n// ─── Snapshot recording helpers ──────────────────────────────────────────────\n\n/**\n * Convert a test ID (e.g. Playwright titlePath) into a filesystem-safe slug\n * suitable for use as a directory name in snapshot-style recording.\n */\nexport function slugifyTestId(testId: string): string {\n return testId\n .replace(/^.*?\\.(?:spec|test|e2e)\\.(?:tsx|ts|jsx|js|mjs|cjs)(?=\\s|›|$)\\s*›?\\s*/i, \"\") // strip test file extension prefix\n .replace(/\\s*[›>]\\s*/g, \"--\") // Playwright titlePath separator → double dash\n .replace(/[^\\w-]/g, \"-\") // non-word chars → dash\n .replace(/-{3,}/g, \"--\") // collapse 3+ dashes to double\n .replace(/^-+|-+$/g, \"\") // trim leading/trailing dashes\n .toLowerCase();\n}\n\n// ─── Embedding helpers ─────────────────────────────────────────────────────\n\nconst DEFAULT_EMBEDDING_DIMENSIONS = 1536;\n\n/**\n * Generate a deterministic embedding vector from input text.\n * Hashes the input with SHA-256 and spreads the hash bytes across\n * the requested number of dimensions, producing values in [-1, 1].\n */\nexport function generateDeterministicEmbedding(\n input: string,\n dimensions: number = DEFAULT_EMBEDDING_DIMENSIONS,\n): number[] {\n let currentHash = createHash(\"sha256\").update(input).digest();\n const embedding: number[] = new Array(dimensions);\n for (let i = 0; i < dimensions; i++) {\n if (i > 0 && i % 32 === 0) {\n currentHash = createHash(\"sha256\").update(currentHash).digest();\n }\n // Map 0-255 → -1.0 to 1.0\n embedding[i] = currentHash[i % 32] / 127.5 - 1;\n }\n return embedding;\n}\n\nexport interface EmbeddingAPIResponse {\n object: \"list\";\n data: { object: \"embedding\"; index: number; embedding: number[] }[];\n model: string;\n usage: { prompt_tokens: number; total_tokens: number };\n}\n\n/**\n * Build an OpenAI-format embeddings API response for one or more inputs.\n */\nexport function buildEmbeddingResponse(\n embeddings: number[][],\n model: string,\n): EmbeddingAPIResponse {\n return {\n object: \"list\",\n data: embeddings.map((embedding, index) => ({\n object: \"embedding\" as const,\n index,\n embedding,\n })),\n model,\n usage: { prompt_tokens: 0, total_tokens: 0 },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"helpers.js","names":[],"sources":["../src/helpers.ts"],"sourcesContent":["import { createHash, randomBytes } from \"node:crypto\";\nimport type * as http from \"node:http\";\nimport type { IncomingHttpHeaders } from \"node:http\";\nimport type {\n ChatCompletionRequest,\n Fixture,\n FixtureResponse,\n ResponseFactory,\n TextResponse,\n ToolCallResponse,\n ContentWithToolCallsResponse,\n ErrorResponse,\n EmbeddingResponse,\n ImageResponse,\n AudioResponse,\n TranscriptionResponse,\n VideoResponse,\n RawJSONResponse,\n SSEChunk,\n ToolCall,\n ChatCompletion,\n ResponseOverrides,\n} from \"./types.js\";\n\nconst REDACTED_HEADERS = new Set([\"authorization\", \"x-api-key\", \"api-key\"]);\n\n/**\n * Resolve effective strict mode from per-request header and server default.\n * Header values override the server default — same precedence pattern as chaos\n * config headers (see resolveChaosConfig in chaos.ts).\n *\n * Header: `X-AIMock-Strict` — \"true\"/\"1\" → strict on, \"false\"/\"0\" → strict off.\n * When absent or unrecognised, falls back to the server-level default.\n */\nexport function resolveStrictMode(\n serverDefault: boolean | undefined,\n rawHeaders?: IncomingHttpHeaders,\n): boolean {\n if (rawHeaders) {\n const header = rawHeaders[\"x-aimock-strict\"];\n const val = typeof header === \"string\" ? header : Array.isArray(header) ? header[0] : undefined;\n if (val === \"true\" || val === \"1\") return true;\n if (val === \"false\" || val === \"0\") return false;\n }\n return serverDefault ?? false;\n}\n\n/**\n * Returns `true` or `false` when the X-AIMock-Strict header overrides the\n * server default, or `undefined` when it doesn't. Designed to be spread\n * directly into a journal entry's `response` object:\n *\n * response: { status, fixture, ...strictOverrideField(defaults.strict, req.headers) }\n */\nexport function strictOverrideField(\n serverDefault: boolean | undefined,\n rawHeaders?: IncomingHttpHeaders,\n): { strictOverride?: boolean } {\n const effective = resolveStrictMode(serverDefault, rawHeaders);\n if (effective !== (serverDefault ?? false)) {\n return { strictOverride: effective };\n }\n return {};\n}\n\nexport function flattenHeaders(headers: http.IncomingHttpHeaders): Record<string, string> {\n const flat: Record<string, string> = {};\n for (const [key, value] of Object.entries(headers)) {\n if (value === undefined) continue;\n if (REDACTED_HEADERS.has(key.toLowerCase())) {\n flat[key] = \"[REDACTED]\";\n } else {\n flat[key] = Array.isArray(value) ? value.join(\", \") : value;\n }\n }\n return flat;\n}\n\nexport function isResponseFactory(r: FixtureResponse | ResponseFactory): r is ResponseFactory {\n return typeof r === \"function\";\n}\n\nexport async function resolveResponse(\n fixture: Fixture,\n request: ChatCompletionRequest,\n): Promise<FixtureResponse> {\n if (typeof fixture.response === \"function\") {\n const raw = await fixture.response(request);\n return normalizeFactoryResponse(raw);\n }\n return fixture.response;\n}\n\nfunction normalizeFactoryResponse(raw: FixtureResponse): FixtureResponse {\n const r = { ...raw } as Record<string, unknown>;\n if (typeof r.content === \"object\" && r.content !== null) {\n r.content = JSON.stringify(r.content);\n }\n if (Array.isArray(r.toolCalls)) {\n r.toolCalls = (r.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 return r as unknown as FixtureResponse;\n}\n\nexport function generateId(prefix = \"chatcmpl\"): string {\n return `${prefix}-${randomBytes(12).toString(\"base64url\")}`;\n}\n\nexport function generateToolCallId(): string {\n return `call_${randomBytes(12).toString(\"base64url\")}`;\n}\n\nexport function generateMessageId(): string {\n return `msg_${randomBytes(12).toString(\"base64url\")}`;\n}\n\nexport function generateToolUseId(): string {\n return `toolu_${randomBytes(12).toString(\"base64url\")}`;\n}\n\nexport function isTextResponse(r: FixtureResponse): r is TextResponse {\n return \"content\" in r && typeof (r as TextResponse).content === \"string\" && !(\"toolCalls\" in r);\n}\n\nexport function isToolCallResponse(r: FixtureResponse): r is ToolCallResponse {\n return (\n \"toolCalls\" in r &&\n Array.isArray((r as ToolCallResponse).toolCalls) &&\n !(\"content\" in r && typeof (r as unknown as Record<string, unknown>).content === \"string\")\n );\n}\n\nexport function isContentWithToolCallsResponse(\n r: FixtureResponse,\n): r is ContentWithToolCallsResponse {\n return (\n \"content\" in r &&\n typeof (r as ContentWithToolCallsResponse).content === \"string\" &&\n \"toolCalls\" in r &&\n Array.isArray((r as ContentWithToolCallsResponse).toolCalls)\n );\n}\n\nexport function isErrorResponse(r: FixtureResponse): r is ErrorResponse {\n return (\n \"error\" in r &&\n (r as ErrorResponse).error !== null &&\n typeof (r as ErrorResponse).error === \"object\"\n );\n}\n\nexport function isEmbeddingResponse(r: FixtureResponse): r is EmbeddingResponse {\n return \"embedding\" in r && Array.isArray((r as EmbeddingResponse).embedding);\n}\n\nexport function isImageResponse(r: FixtureResponse): r is ImageResponse {\n return (\n (\"image\" in r && r.image != null) ||\n (\"images\" in r && Array.isArray((r as ImageResponse).images))\n );\n}\n\nexport function isAudioResponse(r: FixtureResponse): r is AudioResponse {\n if (!(\"audio\" in r)) return false;\n const a = (r as AudioResponse).audio;\n return typeof a === \"string\" || (typeof a === \"object\" && a !== null && \"b64Json\" in a);\n}\n\n/**\n * Map audio format shorthand to MIME content types.\n * Shared between speech, ElevenLabs, and fal audio handlers.\n */\nexport const FORMAT_TO_CONTENT_TYPE: Record<string, string> = {\n mp3: \"audio/mpeg\",\n opus: \"audio/opus\",\n aac: \"audio/aac\",\n flac: \"audio/flac\",\n wav: \"audio/wav\",\n pcm: \"audio/pcm\",\n};\n\n/**\n * Resolve a format string (e.g. \"mp3\", \"opus\") to its MIME content type.\n * Falls back to \"application/octet-stream\" for unknown formats.\n */\nexport function formatToMime(format: string): string {\n return FORMAT_TO_CONTENT_TYPE[format] ?? \"application/octet-stream\";\n}\n\nexport function isTranscriptionResponse(r: FixtureResponse): r is TranscriptionResponse {\n return (\n \"transcription\" in r &&\n (r as TranscriptionResponse).transcription != null &&\n typeof (r as TranscriptionResponse).transcription === \"object\"\n );\n}\n\nexport function isVideoResponse(r: FixtureResponse): r is VideoResponse {\n return (\n \"video\" in r &&\n (r as VideoResponse).video != null &&\n typeof (r as VideoResponse).video === \"object\"\n );\n}\n\nexport function isJSONResponse(r: FixtureResponse): r is RawJSONResponse {\n return \"json\" in r && (r as RawJSONResponse).json !== undefined;\n}\n\nexport function extractOverrides(\n response: TextResponse | ToolCallResponse | ContentWithToolCallsResponse,\n): ResponseOverrides {\n const r = response;\n return {\n ...(r.id !== undefined && { id: r.id }),\n ...(r.created !== undefined && { created: r.created }),\n ...(r.model !== undefined && { model: r.model }),\n ...(r.usage !== undefined && { usage: r.usage }),\n ...(r.systemFingerprint !== undefined && { systemFingerprint: r.systemFingerprint }),\n ...(r.finishReason !== undefined && { finishReason: r.finishReason }),\n ...(r.role !== undefined && { role: r.role }),\n };\n}\n\nexport function buildTextChunks(\n content: string,\n model: string,\n chunkSize: number,\n reasoning?: string,\n overrides?: ResponseOverrides,\n): SSEChunk[] {\n const id = overrides?.id ?? generateId();\n const created = overrides?.created ?? Math.floor(Date.now() / 1000);\n const effectiveModel = overrides?.model ?? model;\n const chunks: SSEChunk[] = [];\n const fingerprint = overrides?.systemFingerprint;\n\n // Reasoning chunks (emitted before content, OpenRouter format)\n if (reasoning) {\n for (let i = 0; i < reasoning.length; i += chunkSize) {\n const slice = reasoning.slice(i, i + chunkSize);\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n { index: 0, delta: { reasoning_content: slice }, logprobs: null, finish_reason: null },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n }\n }\n\n // Role chunk\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: { role: overrides?.role ?? \"assistant\", content: \"\" },\n logprobs: null,\n finish_reason: null,\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n // Content chunks\n for (let i = 0; i < content.length; i += chunkSize) {\n const slice = content.slice(i, i + chunkSize);\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [{ index: 0, delta: { content: slice }, logprobs: null, finish_reason: null }],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n }\n\n // Finish chunk\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n { index: 0, delta: {}, logprobs: null, finish_reason: overrides?.finishReason ?? \"stop\" },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n return chunks;\n}\n\nexport function buildToolCallChunks(\n toolCalls: ToolCall[],\n model: string,\n chunkSize: number,\n overrides?: ResponseOverrides,\n): SSEChunk[] {\n const id = overrides?.id ?? generateId();\n const created = overrides?.created ?? Math.floor(Date.now() / 1000);\n const effectiveModel = overrides?.model ?? model;\n const chunks: SSEChunk[] = [];\n const fingerprint = overrides?.systemFingerprint;\n\n // Role chunk\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: { role: overrides?.role ?? \"assistant\", content: null },\n logprobs: null,\n finish_reason: null,\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n // Tool call chunks — one initial chunk per tool call, then argument chunks\n for (let tcIdx = 0; tcIdx < toolCalls.length; tcIdx++) {\n const tc = toolCalls[tcIdx];\n const tcId = tc.id || generateToolCallId();\n\n // Initial tool call chunk (id + function name)\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: {\n tool_calls: [\n {\n index: tcIdx,\n id: tcId,\n type: \"function\",\n function: { name: tc.name, arguments: \"\" },\n },\n ],\n },\n logprobs: null,\n finish_reason: null,\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n // Argument streaming chunks\n const args = tc.arguments;\n for (let i = 0; i < args.length; i += chunkSize) {\n const slice = args.slice(i, i + chunkSize);\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: {\n tool_calls: [{ index: tcIdx, function: { arguments: slice } }],\n },\n logprobs: null,\n finish_reason: null,\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n }\n }\n\n // Finish chunk\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: {},\n logprobs: null,\n finish_reason: overrides?.finishReason ?? \"tool_calls\",\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n return chunks;\n}\n\n// Non-streaming response builders\n\nexport function buildTextCompletion(\n content: string,\n model: string,\n reasoning?: string,\n overrides?: ResponseOverrides,\n): ChatCompletion {\n const defaultUsage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };\n return {\n id: overrides?.id ?? generateId(),\n object: \"chat.completion\",\n created: overrides?.created ?? Math.floor(Date.now() / 1000),\n model: overrides?.model ?? model,\n choices: [\n {\n index: 0,\n message: {\n role: overrides?.role ?? \"assistant\",\n content,\n refusal: null,\n ...(reasoning ? { reasoning_content: reasoning } : {}),\n },\n logprobs: null,\n finish_reason: overrides?.finishReason ?? \"stop\",\n },\n ],\n usage: overrides?.usage\n ? {\n prompt_tokens: overrides.usage.prompt_tokens ?? defaultUsage.prompt_tokens,\n completion_tokens: overrides.usage.completion_tokens ?? defaultUsage.completion_tokens,\n total_tokens:\n overrides.usage.total_tokens ??\n (overrides.usage.prompt_tokens ?? 0) + (overrides.usage.completion_tokens ?? 0),\n }\n : defaultUsage,\n ...(overrides?.systemFingerprint !== undefined && {\n system_fingerprint: overrides.systemFingerprint,\n }),\n };\n}\n\nexport function buildToolCallCompletion(\n toolCalls: ToolCall[],\n model: string,\n overrides?: ResponseOverrides,\n): ChatCompletion {\n const defaultUsage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };\n return {\n id: overrides?.id ?? generateId(),\n object: \"chat.completion\",\n created: overrides?.created ?? Math.floor(Date.now() / 1000),\n model: overrides?.model ?? model,\n choices: [\n {\n index: 0,\n message: {\n role: overrides?.role ?? \"assistant\",\n content: null,\n refusal: null,\n tool_calls: toolCalls.map((tc) => ({\n id: tc.id || generateToolCallId(),\n type: \"function\" as const,\n function: { name: tc.name, arguments: tc.arguments },\n })),\n },\n logprobs: null,\n finish_reason: overrides?.finishReason ?? \"tool_calls\",\n },\n ],\n usage: overrides?.usage\n ? {\n prompt_tokens: overrides.usage.prompt_tokens ?? defaultUsage.prompt_tokens,\n completion_tokens: overrides.usage.completion_tokens ?? defaultUsage.completion_tokens,\n total_tokens:\n overrides.usage.total_tokens ??\n (overrides.usage.prompt_tokens ?? 0) + (overrides.usage.completion_tokens ?? 0),\n }\n : defaultUsage,\n ...(overrides?.systemFingerprint !== undefined && {\n system_fingerprint: overrides.systemFingerprint,\n }),\n };\n}\n\nexport function buildContentWithToolCallsChunks(\n content: string,\n toolCalls: ToolCall[],\n model: string,\n chunkSize: number,\n reasoning?: string,\n overrides?: ResponseOverrides,\n): SSEChunk[] {\n const id = overrides?.id ?? generateId();\n const created = overrides?.created ?? Math.floor(Date.now() / 1000);\n const effectiveModel = overrides?.model ?? model;\n const chunks: SSEChunk[] = [];\n const fingerprint = overrides?.systemFingerprint;\n\n // Reasoning chunks (emitted before content, OpenRouter format)\n if (reasoning) {\n for (let i = 0; i < reasoning.length; i += chunkSize) {\n const slice = reasoning.slice(i, i + chunkSize);\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n { index: 0, delta: { reasoning_content: slice }, logprobs: null, finish_reason: null },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n }\n }\n\n // Role chunk\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: { role: overrides?.role ?? \"assistant\", content: \"\" },\n logprobs: null,\n finish_reason: null,\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n // Content chunks\n for (let i = 0; i < content.length; i += chunkSize) {\n const slice = content.slice(i, i + chunkSize);\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [{ index: 0, delta: { content: slice }, logprobs: null, finish_reason: null }],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n }\n\n // Tool call chunks — one initial chunk per tool call, then argument chunks\n for (let tcIdx = 0; tcIdx < toolCalls.length; tcIdx++) {\n const tc = toolCalls[tcIdx];\n const tcId = tc.id || generateToolCallId();\n\n // Initial tool call chunk (id + function name)\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: {\n tool_calls: [\n {\n index: tcIdx,\n id: tcId,\n type: \"function\",\n function: { name: tc.name, arguments: \"\" },\n },\n ],\n },\n logprobs: null,\n finish_reason: null,\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n // Argument streaming chunks\n const args = tc.arguments;\n for (let i = 0; i < args.length; i += chunkSize) {\n const slice = args.slice(i, i + chunkSize);\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: {\n tool_calls: [{ index: tcIdx, function: { arguments: slice } }],\n },\n logprobs: null,\n finish_reason: null,\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n }\n }\n\n // Finish chunk\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: {},\n logprobs: null,\n finish_reason: overrides?.finishReason ?? \"tool_calls\",\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n return chunks;\n}\n\nexport function buildContentWithToolCallsCompletion(\n content: string,\n toolCalls: ToolCall[],\n model: string,\n reasoning?: string,\n overrides?: ResponseOverrides,\n): ChatCompletion {\n const defaultUsage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };\n return {\n id: overrides?.id ?? generateId(),\n object: \"chat.completion\",\n created: overrides?.created ?? Math.floor(Date.now() / 1000),\n model: overrides?.model ?? model,\n choices: [\n {\n index: 0,\n message: {\n role: overrides?.role ?? \"assistant\",\n content,\n refusal: null,\n ...(reasoning ? { reasoning_content: reasoning } : {}),\n tool_calls: toolCalls.map((tc) => ({\n id: tc.id || generateToolCallId(),\n type: \"function\" as const,\n function: { name: tc.name, arguments: tc.arguments },\n })),\n },\n logprobs: null,\n finish_reason: overrides?.finishReason ?? \"tool_calls\",\n },\n ],\n usage: overrides?.usage\n ? {\n prompt_tokens: overrides.usage.prompt_tokens ?? defaultUsage.prompt_tokens,\n completion_tokens: overrides.usage.completion_tokens ?? defaultUsage.completion_tokens,\n total_tokens:\n overrides.usage.total_tokens ??\n (overrides.usage.prompt_tokens ?? 0) + (overrides.usage.completion_tokens ?? 0),\n }\n : defaultUsage,\n ...(overrides?.systemFingerprint !== undefined && {\n system_fingerprint: overrides.systemFingerprint,\n }),\n };\n}\n\n// ─── HTTP helpers ─────────────────────────────────────────────────────────\n\nexport function readBody(req: http.IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => resolve(Buffer.concat(chunks).toString()));\n req.on(\"error\", reject);\n });\n}\n\n// ─── Pattern matching ─────────────────────────────────────────────────────\n\n/**\n * Case-insensitive substring/regex match used for search, rerank, and\n * moderation endpoints where exact casing rarely matters. String patterns\n * are lowercased on both sides before comparison.\n *\n * Note: This intentionally differs from the case-sensitive matching in\n * {@link matchFixture} (router.ts), where fixture authors expect exact\n * string matching against chat completion user messages.\n */\nexport function matchesPattern(text: string, pattern: string | RegExp): boolean {\n if (typeof pattern === \"string\") {\n return text.toLowerCase().includes(pattern.toLowerCase());\n }\n return pattern.test(text);\n}\n\nexport function getTestId(req: http.IncomingMessage): string {\n const headerValue = req.headers[\"x-test-id\"];\n if (Array.isArray(headerValue)) {\n if (headerValue.length > 0 && headerValue[0]) return headerValue[0];\n } else if (typeof headerValue === \"string\" && headerValue) {\n return headerValue;\n }\n\n const url = req.url ?? \"/\";\n const qIdx = url.indexOf(\"?\");\n if (qIdx !== -1) {\n const params = new URLSearchParams(url.slice(qIdx + 1));\n const queryValue = params.get(\"testId\");\n if (queryValue) return queryValue;\n }\n\n // Duplicated from journal.ts DEFAULT_TEST_ID — importing it here would create\n // a circular dependency (journal.ts imports from helpers.ts).\n return \"__default__\";\n}\n\n// ─── Snapshot recording helpers ──────────────────────────────────────────────\n\n/**\n * Convert a test ID (e.g. Playwright titlePath) into a filesystem-safe slug\n * suitable for use as a directory name in snapshot-style recording.\n */\nexport function slugifyTestId(testId: string): string {\n return testId\n .replace(/^.*?\\.(?:spec|test|e2e)\\.(?:tsx|ts|jsx|js|mjs|cjs)(?=\\s|›|$)\\s*›?\\s*/i, \"\") // strip test file extension prefix\n .replace(/\\s*[›>]\\s*/g, \"--\") // Playwright titlePath separator → double dash\n .replace(/[^\\w-]/g, \"-\") // non-word chars → dash\n .replace(/-{3,}/g, \"--\") // collapse 3+ dashes to double\n .replace(/^-+|-+$/g, \"\") // trim leading/trailing dashes\n .toLowerCase();\n}\n\n// ─── Embedding helpers ─────────────────────────────────────────────────────\n\nconst DEFAULT_EMBEDDING_DIMENSIONS = 1536;\n\n/**\n * Generate a deterministic embedding vector from input text.\n * Hashes the input with SHA-256 and spreads the hash bytes across\n * the requested number of dimensions, producing values in [-1, 1].\n */\nexport function generateDeterministicEmbedding(\n input: string,\n dimensions: number = DEFAULT_EMBEDDING_DIMENSIONS,\n): number[] {\n let currentHash = createHash(\"sha256\").update(input).digest();\n const embedding: number[] = new Array(dimensions);\n for (let i = 0; i < dimensions; i++) {\n if (i > 0 && i % 32 === 0) {\n currentHash = createHash(\"sha256\").update(currentHash).digest();\n }\n // Map 0-255 → -1.0 to 1.0\n embedding[i] = currentHash[i % 32] / 127.5 - 1;\n }\n return embedding;\n}\n\nexport interface EmbeddingAPIResponse {\n object: \"list\";\n data: { object: \"embedding\"; index: number; embedding: number[] }[];\n model: string;\n usage: { prompt_tokens: number; total_tokens: number };\n}\n\n/**\n * Build an OpenAI-format embeddings API response for one or more inputs.\n */\nexport function buildEmbeddingResponse(\n embeddings: number[][],\n model: string,\n): EmbeddingAPIResponse {\n return {\n object: \"list\",\n data: embeddings.map((embedding, index) => ({\n object: \"embedding\" as const,\n index,\n embedding,\n })),\n model,\n usage: { prompt_tokens: 0, total_tokens: 0 },\n };\n}\n"],"mappings":"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"}
package/dist/images.cjs CHANGED
@@ -31,7 +31,8 @@ async function handleImages(req, res, raw, fixtures, journal, defaults, setCorsH
31
31
  prompt = openaiReq.prompt ?? "";
32
32
  model = openaiReq.model ?? "dall-e-3";
33
33
  }
34
- } catch {
34
+ } catch (parseErr) {
35
+ const detail = parseErr instanceof Error ? parseErr.message : "unknown";
35
36
  journal.add({
36
37
  method,
37
38
  path,
@@ -43,7 +44,7 @@ async function handleImages(req, res, raw, fixtures, journal, defaults, setCorsH
43
44
  }
44
45
  });
45
46
  require_sse_writer.writeErrorResponse(res, 400, JSON.stringify({ error: {
46
- message: "Malformed JSON",
47
+ message: `Malformed JSON: ${detail}`,
47
48
  type: "invalid_request_error",
48
49
  code: "invalid_json"
49
50
  } }));
@@ -96,8 +97,9 @@ async function handleImages(req, res, raw, fixtures, journal, defaults, setCorsH
96
97
  return;
97
98
  }
98
99
  }
99
- const strictStatus = defaults.strict ? 503 : 404;
100
- const strictMessage = defaults.strict ? "Strict mode: no fixture matched" : "No fixture matched";
100
+ const effectiveStrict = require_helpers.resolveStrictMode(defaults.strict, req.headers);
101
+ const strictStatus = effectiveStrict ? 503 : 404;
102
+ const strictMessage = effectiveStrict ? "Strict mode: no fixture matched" : "No fixture matched";
101
103
  journal.add({
102
104
  method,
103
105
  path,
@@ -105,7 +107,8 @@ async function handleImages(req, res, raw, fixtures, journal, defaults, setCorsH
105
107
  body: syntheticReq,
106
108
  response: {
107
109
  status: strictStatus,
108
- fixture: null
110
+ fixture: null,
111
+ ...require_helpers.strictOverrideField(defaults.strict, req.headers)
109
112
  }
110
113
  });
111
114
  require_sse_writer.writeErrorResponse(res, strictStatus, JSON.stringify({ error: {
@@ -1 +1 @@
1
- {"version":3,"file":"images.cjs","names":["flattenHeaders","getTestId","matchFixture","applyChaos","proxyAndRecord","resolveResponse","isErrorResponse","isImageResponse"],"sources":["../src/images.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { ChatCompletionRequest, Fixture, HandlerDefaults } from \"./types.js\";\nimport {\n isImageResponse,\n isErrorResponse,\n flattenHeaders,\n getTestId,\n resolveResponse,\n} from \"./helpers.js\";\nimport { matchFixture } 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\ninterface OpenAIImageRequest {\n model?: string;\n prompt: string;\n n?: number;\n size?: string;\n response_format?: \"url\" | \"b64_json\";\n [key: string]: unknown;\n}\n\ninterface GeminiPredictRequest {\n instances: Array<{ prompt: string }>;\n parameters?: { sampleCount?: number };\n [key: string]: unknown;\n}\n\nfunction buildSyntheticRequest(model: string, prompt: string): ChatCompletionRequest {\n return {\n model,\n messages: [{ role: \"user\", content: prompt }],\n _endpointType: \"image\",\n };\n}\n\nexport async function handleImages(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n format: \"openai\" | \"gemini\" = \"openai\",\n geminiModel?: string,\n): Promise<void> {\n setCorsHeaders(res);\n const path = req.url ?? \"/v1/images/generations\";\n const method = req.method ?? \"POST\";\n\n let model: string;\n let prompt: string;\n\n try {\n const body = JSON.parse(raw);\n if (format === \"gemini\") {\n const geminiReq = body as GeminiPredictRequest;\n prompt = geminiReq.instances?.[0]?.prompt ?? \"\";\n model = geminiModel ?? \"imagen\";\n } else {\n const openaiReq = body as OpenAIImageRequest;\n prompt = openaiReq.prompt ?? \"\";\n model = openaiReq.model ?? \"dall-e-3\";\n }\n } catch {\n journal.add({\n method,\n path,\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: { message: \"Malformed JSON\", type: \"invalid_request_error\", code: \"invalid_json\" },\n }),\n );\n return;\n }\n\n if (!prompt) {\n journal.add({\n method,\n path,\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: { message: \"Missing required parameter: 'prompt'\", type: \"invalid_request_error\" },\n }),\n );\n return;\n }\n\n const syntheticReq = buildSyntheticRequest(model, prompt);\n const testId = getTestId(req);\n const fixture = matchFixture(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n defaults.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 { method, path, headers: flattenHeaders(req.headers), body: syntheticReq },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (!fixture) {\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n format === \"gemini\" ? \"gemini\" : \"openai\",\n req.url ?? \"/v1/images/generations\",\n fixtures,\n defaults,\n raw,\n );\n if (outcome !== \"not_configured\") {\n journal.add({\n method,\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 const strictStatus = defaults.strict ? 503 : 404;\n const strictMessage = defaults.strict\n ? \"Strict mode: no fixture matched\"\n : \"No fixture matched\";\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: strictStatus, fixture: null },\n });\n writeErrorResponse(\n res,\n strictStatus,\n JSON.stringify({\n error: { message: strictMessage, type: \"invalid_request_error\", code: \"no_fixture_match\" },\n }),\n );\n return;\n }\n\n const response = await resolveResponse(fixture, syntheticReq);\n\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, JSON.stringify(response));\n return;\n }\n\n if (!isImageResponse(response)) {\n journal.add({\n method,\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: { message: \"Fixture response is not an image type\", type: \"server_error\" },\n }),\n );\n return;\n }\n\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n\n // Normalize to array of image items\n const items = response.images ?? (response.image ? [response.image] : []);\n\n if (format === \"gemini\") {\n const predictions = items.map((item) => ({\n bytesBase64Encoded: item.b64Json ?? \"\",\n mimeType: \"image/png\" as const,\n }));\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ predictions }));\n } else {\n const data = items.map((item) => {\n const entry: Record<string, string> = {};\n if (item.url) entry.url = item.url;\n if (item.b64Json) entry.b64_json = item.b64Json;\n if (item.revisedPrompt) entry.revised_prompt = item.revisedPrompt;\n return entry;\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ created: Math.floor(Date.now() / 1000), data }));\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"images.cjs","names":["flattenHeaders","getTestId","matchFixture","applyChaos","proxyAndRecord","resolveStrictMode","strictOverrideField","resolveResponse","isErrorResponse","isImageResponse"],"sources":["../src/images.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { ChatCompletionRequest, Fixture, HandlerDefaults } from \"./types.js\";\nimport {\n isImageResponse,\n isErrorResponse,\n flattenHeaders,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n} from \"./helpers.js\";\nimport { matchFixture } 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\ninterface OpenAIImageRequest {\n model?: string;\n prompt: string;\n n?: number;\n size?: string;\n response_format?: \"url\" | \"b64_json\";\n [key: string]: unknown;\n}\n\ninterface GeminiPredictRequest {\n instances: Array<{ prompt: string }>;\n parameters?: { sampleCount?: number };\n [key: string]: unknown;\n}\n\nfunction buildSyntheticRequest(model: string, prompt: string): ChatCompletionRequest {\n return {\n model,\n messages: [{ role: \"user\", content: prompt }],\n _endpointType: \"image\",\n };\n}\n\nexport async function handleImages(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n format: \"openai\" | \"gemini\" = \"openai\",\n geminiModel?: string,\n): Promise<void> {\n setCorsHeaders(res);\n const path = req.url ?? \"/v1/images/generations\";\n const method = req.method ?? \"POST\";\n\n let model: string;\n let prompt: string;\n\n try {\n const body = JSON.parse(raw);\n if (format === \"gemini\") {\n const geminiReq = body as GeminiPredictRequest;\n prompt = geminiReq.instances?.[0]?.prompt ?? \"\";\n model = geminiModel ?? \"imagen\";\n } else {\n const openaiReq = body as OpenAIImageRequest;\n prompt = openaiReq.prompt ?? \"\";\n model = openaiReq.model ?? \"dall-e-3\";\n }\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method,\n path,\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: ${detail}`,\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return;\n }\n\n if (!prompt) {\n journal.add({\n method,\n path,\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: { message: \"Missing required parameter: 'prompt'\", type: \"invalid_request_error\" },\n }),\n );\n return;\n }\n\n const syntheticReq = buildSyntheticRequest(model, prompt);\n const testId = getTestId(req);\n const fixture = matchFixture(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n defaults.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 { method, path, headers: flattenHeaders(req.headers), body: syntheticReq },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (!fixture) {\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n format === \"gemini\" ? \"gemini\" : \"openai\",\n req.url ?? \"/v1/images/generations\",\n fixtures,\n defaults,\n raw,\n );\n if (outcome !== \"not_configured\") {\n journal.add({\n method,\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 const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n const strictStatus = effectiveStrict ? 503 : 404;\n const strictMessage = effectiveStrict\n ? \"Strict mode: no fixture matched\"\n : \"No fixture matched\";\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: strictStatus,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n strictStatus,\n JSON.stringify({\n error: { message: strictMessage, type: \"invalid_request_error\", code: \"no_fixture_match\" },\n }),\n );\n return;\n }\n\n const response = await resolveResponse(fixture, syntheticReq);\n\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, JSON.stringify(response));\n return;\n }\n\n if (!isImageResponse(response)) {\n journal.add({\n method,\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: { message: \"Fixture response is not an image type\", type: \"server_error\" },\n }),\n );\n return;\n }\n\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n\n // Normalize to array of image items\n const items = response.images ?? (response.image ? [response.image] : []);\n\n if (format === \"gemini\") {\n const predictions = items.map((item) => ({\n bytesBase64Encoded: item.b64Json ?? \"\",\n mimeType: \"image/png\" as const,\n }));\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ predictions }));\n } else {\n const data = items.map((item) => {\n const entry: Record<string, string> = {};\n if (item.url) entry.url = item.url;\n if (item.b64Json) entry.b64_json = item.b64Json;\n if (item.revisedPrompt) entry.revised_prompt = item.revisedPrompt;\n return entry;\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ created: Math.floor(Date.now() / 1000), data }));\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"images.d.cts","names":[],"sources":["../src/images.ts"],"sourcesContent":[],"mappings":";;;;;iBAsCsB,YAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,uCAEA,oBACD,mBACC,uCACY,MAAA,CAAK,8EAG1B"}
1
+ {"version":3,"file":"images.d.cts","names":[],"sources":["../src/images.ts"],"sourcesContent":[],"mappings":";;;;;iBAwCsB,YAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,uCAEA,oBACD,mBACC,uCACY,MAAA,CAAK,8EAG1B"}
@@ -1 +1 @@
1
- {"version":3,"file":"images.d.ts","names":[],"sources":["../src/images.ts"],"sourcesContent":[],"mappings":";;;;;iBAsCsB,YAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,uCAEA,oBACD,mBACC,uCACY,MAAA,CAAK,8EAG1B"}
1
+ {"version":3,"file":"images.d.ts","names":[],"sources":["../src/images.ts"],"sourcesContent":[],"mappings":";;;;;iBAwCsB,YAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,uCAEA,oBACD,mBACC,uCACY,MAAA,CAAK,8EAG1B"}
package/dist/images.js CHANGED
@@ -1,4 +1,4 @@
1
- import { flattenHeaders, getTestId, isErrorResponse, isImageResponse, resolveResponse } from "./helpers.js";
1
+ import { flattenHeaders, getTestId, isErrorResponse, isImageResponse, resolveResponse, resolveStrictMode, strictOverrideField } from "./helpers.js";
2
2
  import { matchFixture } from "./router.js";
3
3
  import { writeErrorResponse } from "./sse-writer.js";
4
4
  import { applyChaos } from "./chaos.js";
@@ -31,7 +31,8 @@ async function handleImages(req, res, raw, fixtures, journal, defaults, setCorsH
31
31
  prompt = openaiReq.prompt ?? "";
32
32
  model = openaiReq.model ?? "dall-e-3";
33
33
  }
34
- } catch {
34
+ } catch (parseErr) {
35
+ const detail = parseErr instanceof Error ? parseErr.message : "unknown";
35
36
  journal.add({
36
37
  method,
37
38
  path,
@@ -43,7 +44,7 @@ async function handleImages(req, res, raw, fixtures, journal, defaults, setCorsH
43
44
  }
44
45
  });
45
46
  writeErrorResponse(res, 400, JSON.stringify({ error: {
46
- message: "Malformed JSON",
47
+ message: `Malformed JSON: ${detail}`,
47
48
  type: "invalid_request_error",
48
49
  code: "invalid_json"
49
50
  } }));
@@ -96,8 +97,9 @@ async function handleImages(req, res, raw, fixtures, journal, defaults, setCorsH
96
97
  return;
97
98
  }
98
99
  }
99
- const strictStatus = defaults.strict ? 503 : 404;
100
- const strictMessage = defaults.strict ? "Strict mode: no fixture matched" : "No fixture matched";
100
+ const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);
101
+ const strictStatus = effectiveStrict ? 503 : 404;
102
+ const strictMessage = effectiveStrict ? "Strict mode: no fixture matched" : "No fixture matched";
101
103
  journal.add({
102
104
  method,
103
105
  path,
@@ -105,7 +107,8 @@ async function handleImages(req, res, raw, fixtures, journal, defaults, setCorsH
105
107
  body: syntheticReq,
106
108
  response: {
107
109
  status: strictStatus,
108
- fixture: null
110
+ fixture: null,
111
+ ...strictOverrideField(defaults.strict, req.headers)
109
112
  }
110
113
  });
111
114
  writeErrorResponse(res, strictStatus, JSON.stringify({ error: {
@@ -1 +1 @@
1
- {"version":3,"file":"images.js","names":[],"sources":["../src/images.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { ChatCompletionRequest, Fixture, HandlerDefaults } from \"./types.js\";\nimport {\n isImageResponse,\n isErrorResponse,\n flattenHeaders,\n getTestId,\n resolveResponse,\n} from \"./helpers.js\";\nimport { matchFixture } 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\ninterface OpenAIImageRequest {\n model?: string;\n prompt: string;\n n?: number;\n size?: string;\n response_format?: \"url\" | \"b64_json\";\n [key: string]: unknown;\n}\n\ninterface GeminiPredictRequest {\n instances: Array<{ prompt: string }>;\n parameters?: { sampleCount?: number };\n [key: string]: unknown;\n}\n\nfunction buildSyntheticRequest(model: string, prompt: string): ChatCompletionRequest {\n return {\n model,\n messages: [{ role: \"user\", content: prompt }],\n _endpointType: \"image\",\n };\n}\n\nexport async function handleImages(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n format: \"openai\" | \"gemini\" = \"openai\",\n geminiModel?: string,\n): Promise<void> {\n setCorsHeaders(res);\n const path = req.url ?? \"/v1/images/generations\";\n const method = req.method ?? \"POST\";\n\n let model: string;\n let prompt: string;\n\n try {\n const body = JSON.parse(raw);\n if (format === \"gemini\") {\n const geminiReq = body as GeminiPredictRequest;\n prompt = geminiReq.instances?.[0]?.prompt ?? \"\";\n model = geminiModel ?? \"imagen\";\n } else {\n const openaiReq = body as OpenAIImageRequest;\n prompt = openaiReq.prompt ?? \"\";\n model = openaiReq.model ?? \"dall-e-3\";\n }\n } catch {\n journal.add({\n method,\n path,\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: { message: \"Malformed JSON\", type: \"invalid_request_error\", code: \"invalid_json\" },\n }),\n );\n return;\n }\n\n if (!prompt) {\n journal.add({\n method,\n path,\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: { message: \"Missing required parameter: 'prompt'\", type: \"invalid_request_error\" },\n }),\n );\n return;\n }\n\n const syntheticReq = buildSyntheticRequest(model, prompt);\n const testId = getTestId(req);\n const fixture = matchFixture(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n defaults.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 { method, path, headers: flattenHeaders(req.headers), body: syntheticReq },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (!fixture) {\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n format === \"gemini\" ? \"gemini\" : \"openai\",\n req.url ?? \"/v1/images/generations\",\n fixtures,\n defaults,\n raw,\n );\n if (outcome !== \"not_configured\") {\n journal.add({\n method,\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 const strictStatus = defaults.strict ? 503 : 404;\n const strictMessage = defaults.strict\n ? \"Strict mode: no fixture matched\"\n : \"No fixture matched\";\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: strictStatus, fixture: null },\n });\n writeErrorResponse(\n res,\n strictStatus,\n JSON.stringify({\n error: { message: strictMessage, type: \"invalid_request_error\", code: \"no_fixture_match\" },\n }),\n );\n return;\n }\n\n const response = await resolveResponse(fixture, syntheticReq);\n\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, JSON.stringify(response));\n return;\n }\n\n if (!isImageResponse(response)) {\n journal.add({\n method,\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: { message: \"Fixture response is not an image type\", type: \"server_error\" },\n }),\n );\n return;\n }\n\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n\n // Normalize to array of image items\n const items = response.images ?? (response.image ? [response.image] : []);\n\n if (format === \"gemini\") {\n const predictions = items.map((item) => ({\n bytesBase64Encoded: item.b64Json ?? \"\",\n mimeType: \"image/png\" as const,\n }));\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ predictions }));\n } else {\n const data = items.map((item) => {\n const entry: Record<string, string> = {};\n if (item.url) entry.url = item.url;\n if (item.b64Json) entry.b64_json = item.b64Json;\n if (item.revisedPrompt) entry.revised_prompt = item.revisedPrompt;\n return entry;\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ created: Math.floor(Date.now() / 1000), data }));\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"images.js","names":[],"sources":["../src/images.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { ChatCompletionRequest, Fixture, HandlerDefaults } from \"./types.js\";\nimport {\n isImageResponse,\n isErrorResponse,\n flattenHeaders,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n} from \"./helpers.js\";\nimport { matchFixture } 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\ninterface OpenAIImageRequest {\n model?: string;\n prompt: string;\n n?: number;\n size?: string;\n response_format?: \"url\" | \"b64_json\";\n [key: string]: unknown;\n}\n\ninterface GeminiPredictRequest {\n instances: Array<{ prompt: string }>;\n parameters?: { sampleCount?: number };\n [key: string]: unknown;\n}\n\nfunction buildSyntheticRequest(model: string, prompt: string): ChatCompletionRequest {\n return {\n model,\n messages: [{ role: \"user\", content: prompt }],\n _endpointType: \"image\",\n };\n}\n\nexport async function handleImages(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n format: \"openai\" | \"gemini\" = \"openai\",\n geminiModel?: string,\n): Promise<void> {\n setCorsHeaders(res);\n const path = req.url ?? \"/v1/images/generations\";\n const method = req.method ?? \"POST\";\n\n let model: string;\n let prompt: string;\n\n try {\n const body = JSON.parse(raw);\n if (format === \"gemini\") {\n const geminiReq = body as GeminiPredictRequest;\n prompt = geminiReq.instances?.[0]?.prompt ?? \"\";\n model = geminiModel ?? \"imagen\";\n } else {\n const openaiReq = body as OpenAIImageRequest;\n prompt = openaiReq.prompt ?? \"\";\n model = openaiReq.model ?? \"dall-e-3\";\n }\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method,\n path,\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: ${detail}`,\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return;\n }\n\n if (!prompt) {\n journal.add({\n method,\n path,\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: { message: \"Missing required parameter: 'prompt'\", type: \"invalid_request_error\" },\n }),\n );\n return;\n }\n\n const syntheticReq = buildSyntheticRequest(model, prompt);\n const testId = getTestId(req);\n const fixture = matchFixture(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n defaults.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 { method, path, headers: flattenHeaders(req.headers), body: syntheticReq },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (!fixture) {\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n format === \"gemini\" ? \"gemini\" : \"openai\",\n req.url ?? \"/v1/images/generations\",\n fixtures,\n defaults,\n raw,\n );\n if (outcome !== \"not_configured\") {\n journal.add({\n method,\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 const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n const strictStatus = effectiveStrict ? 503 : 404;\n const strictMessage = effectiveStrict\n ? \"Strict mode: no fixture matched\"\n : \"No fixture matched\";\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: strictStatus,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n strictStatus,\n JSON.stringify({\n error: { message: strictMessage, type: \"invalid_request_error\", code: \"no_fixture_match\" },\n }),\n );\n return;\n }\n\n const response = await resolveResponse(fixture, syntheticReq);\n\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, JSON.stringify(response));\n return;\n }\n\n if (!isImageResponse(response)) {\n journal.add({\n method,\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: { message: \"Fixture response is not an image type\", type: \"server_error\" },\n }),\n );\n return;\n }\n\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n\n // Normalize to array of image items\n const items = response.images ?? (response.image ? [response.image] : []);\n\n if (format === \"gemini\") {\n const predictions = items.map((item) => ({\n bytesBase64Encoded: item.b64Json ?? \"\",\n mimeType: \"image/png\" as const,\n }));\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ predictions }));\n } else {\n const data = items.map((item) => {\n const entry: Record<string, string> = {};\n if (item.url) entry.url = item.url;\n if (item.b64Json) entry.b64_json = item.b64Json;\n if (item.revisedPrompt) entry.revised_prompt = item.revisedPrompt;\n return entry;\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ created: Math.floor(Date.now() / 1000), data }));\n }\n}\n"],"mappings":"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"}
package/dist/mcp-mock.cjs CHANGED
@@ -114,7 +114,7 @@ var MCPMock = class {
114
114
  if (!res.headersSent) {
115
115
  res.writeHead(500);
116
116
  res.end("Internal server error");
117
- }
117
+ } else if (!res.writableEnded) res.end();
118
118
  });
119
119
  });
120
120
  });
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-mock.cjs","names":["readBody","flattenHeaders","http","createMCPRequestHandler"],"sources":["../src/mcp-mock.ts"],"sourcesContent":["import * as http from \"node:http\";\nimport type { Mountable } from \"./types.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { MetricsRegistry } from \"./metrics.js\";\nimport type {\n MCPMockOptions,\n MCPToolDefinition,\n MCPResourceDefinition,\n MCPResourceContent,\n MCPPromptDefinition,\n MCPPromptResult,\n MCPContent,\n MCPSession,\n} from \"./mcp-types.js\";\nimport { createMCPRequestHandler, type MCPState } from \"./mcp-handler.js\";\nimport { flattenHeaders, readBody } from \"./helpers.js\";\n\nexport class MCPMock implements Mountable {\n private tools: Map<\n string,\n { def: MCPToolDefinition; handler?: (...args: unknown[]) => unknown }\n > = new Map();\n private resources: Map<string, { def: MCPResourceDefinition; content?: MCPResourceContent }> =\n new Map();\n private prompts: Map<\n string,\n {\n def: MCPPromptDefinition;\n handler?: (...args: unknown[]) => MCPPromptResult | Promise<MCPPromptResult>;\n }\n > = new Map();\n private sessions: Map<string, MCPSession> = new Map();\n private server: http.Server | null = null;\n private journal: Journal | null = null;\n private registry: MetricsRegistry | null = null;\n private options: MCPMockOptions;\n private requestHandler: ReturnType<typeof createMCPRequestHandler>;\n\n constructor(options?: MCPMockOptions) {\n this.options = options ?? {};\n this.requestHandler = this.buildHandler();\n }\n\n // ---- Configuration: Tools ----\n\n addTool(def: MCPToolDefinition): this {\n this.tools.set(def.name, { def });\n return this;\n }\n\n onToolCall(\n name: string,\n handler: (args: unknown) => MCPContent[] | string | Promise<MCPContent[] | string>,\n ): this {\n const entry = this.tools.get(name);\n if (entry) {\n entry.handler = handler;\n } else {\n this.tools.set(name, { def: { name }, handler });\n }\n return this;\n }\n\n // ---- Configuration: Resources ----\n\n addResource(def: MCPResourceDefinition, content?: MCPResourceContent): this {\n this.resources.set(def.uri, { def, content });\n return this;\n }\n\n // ---- Configuration: Prompts ----\n\n addPrompt(\n def: MCPPromptDefinition,\n handler?: (args: unknown) => MCPPromptResult | Promise<MCPPromptResult>,\n ): this {\n this.prompts.set(def.name, { def, handler });\n return this;\n }\n\n // ---- Mountable interface ----\n\n async handleRequest(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n pathname: string,\n ): Promise<boolean> {\n // Only handle POST and DELETE to the root of the mount\n if (pathname !== \"/\" && pathname !== \"\") {\n return false;\n }\n if (req.method !== \"POST\" && req.method !== \"DELETE\") {\n return false;\n }\n\n const body = await readBody(req);\n\n // Extract JSON-RPC method for metrics (skip for DELETE — no JSON-RPC body)\n if (this.registry) {\n if (req.method === \"DELETE\") {\n this.registry.incrementCounter(\"aimock_mcp_requests_total\", { method: \"session/delete\" });\n } else {\n try {\n const parsed = JSON.parse(body);\n const method =\n typeof parsed === \"object\" && parsed !== null && \"method\" in parsed\n ? String(parsed.method)\n : \"unknown\";\n this.registry.incrementCounter(\"aimock_mcp_requests_total\", { method });\n } catch {\n this.registry.incrementCounter(\"aimock_mcp_requests_total\", { method: \"unknown\" });\n }\n }\n }\n\n await this.requestHandler(req, res, body);\n\n // Journal the request after the handler completes\n if (this.journal) {\n this.journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"mcp\",\n response: { status: res.statusCode, fixture: null },\n });\n }\n\n return true;\n }\n\n health(): { status: string; [key: string]: unknown } {\n return {\n status: \"ok\",\n tools: this.tools.size,\n resources: this.resources.size,\n prompts: this.prompts.size,\n sessions: this.sessions.size,\n };\n }\n\n setJournal(journal: Journal): void {\n this.journal = journal;\n }\n\n setRegistry(registry: MetricsRegistry): void {\n this.registry = registry;\n }\n\n // ---- Standalone mode ----\n\n async start(): Promise<string> {\n if (this.server) {\n throw new Error(\"Server already started\");\n }\n\n const host = this.options.host ?? \"127.0.0.1\";\n const port = this.options.port ?? 0;\n\n return new Promise((resolve, reject) => {\n const srv = http.createServer((req, res) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => {\n const body = Buffer.concat(chunks).toString();\n\n this.requestHandler(req, res, body)\n .then(() => {\n if (this.journal) {\n this.journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"mcp\",\n response: { status: res.statusCode, fixture: null },\n });\n }\n })\n .catch((err) => {\n console.error(\"MCPMock request error:\", err);\n if (!res.headersSent) {\n res.writeHead(500);\n res.end(\"Internal server error\");\n }\n });\n });\n });\n\n srv.listen(port, host, () => {\n this.server = srv;\n const addr = srv.address();\n if (typeof addr === \"object\" && addr !== null) {\n resolve(`http://${host}:${addr.port}`);\n } else {\n resolve(`http://${host}:${port}`);\n }\n });\n\n srv.on(\"error\", reject);\n });\n }\n\n async stop(): Promise<void> {\n if (!this.server) {\n throw new Error(\"Server not started\");\n }\n const srv = this.server;\n this.server = null;\n await new Promise<void>((resolve, reject) => {\n srv.close((err) => (err ? reject(err) : resolve()));\n });\n }\n\n // ---- Inspection ----\n\n getRequests(): unknown[] {\n if (!this.journal) return [];\n return this.journal.getAll().filter((e) => e.service === \"mcp\");\n }\n\n getSessions(): Map<string, MCPSession> {\n return new Map(this.sessions);\n }\n\n reset(): this {\n this.tools.clear();\n this.resources.clear();\n this.prompts.clear();\n this.sessions.clear();\n this.requestHandler = this.buildHandler();\n return this;\n }\n\n // ---- Internal ----\n\n private buildHandler() {\n const state: MCPState = {\n serverInfo: this.options.serverInfo ?? { name: \"mcp-mock\", version: \"1.0.0\" },\n tools: this.tools,\n resources: this.resources,\n prompts: this.prompts,\n sessions: this.sessions,\n };\n return createMCPRequestHandler(state);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"mcp-mock.cjs","names":["readBody","flattenHeaders","http","createMCPRequestHandler"],"sources":["../src/mcp-mock.ts"],"sourcesContent":["import * as http from \"node:http\";\nimport type { Mountable } from \"./types.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { MetricsRegistry } from \"./metrics.js\";\nimport type {\n MCPMockOptions,\n MCPToolDefinition,\n MCPResourceDefinition,\n MCPResourceContent,\n MCPPromptDefinition,\n MCPPromptResult,\n MCPContent,\n MCPSession,\n} from \"./mcp-types.js\";\nimport { createMCPRequestHandler, type MCPState } from \"./mcp-handler.js\";\nimport { flattenHeaders, readBody } from \"./helpers.js\";\n\nexport class MCPMock implements Mountable {\n private tools: Map<\n string,\n { def: MCPToolDefinition; handler?: (...args: unknown[]) => unknown }\n > = new Map();\n private resources: Map<string, { def: MCPResourceDefinition; content?: MCPResourceContent }> =\n new Map();\n private prompts: Map<\n string,\n {\n def: MCPPromptDefinition;\n handler?: (...args: unknown[]) => MCPPromptResult | Promise<MCPPromptResult>;\n }\n > = new Map();\n private sessions: Map<string, MCPSession> = new Map();\n private server: http.Server | null = null;\n private journal: Journal | null = null;\n private registry: MetricsRegistry | null = null;\n private options: MCPMockOptions;\n private requestHandler: ReturnType<typeof createMCPRequestHandler>;\n\n constructor(options?: MCPMockOptions) {\n this.options = options ?? {};\n this.requestHandler = this.buildHandler();\n }\n\n // ---- Configuration: Tools ----\n\n addTool(def: MCPToolDefinition): this {\n this.tools.set(def.name, { def });\n return this;\n }\n\n onToolCall(\n name: string,\n handler: (args: unknown) => MCPContent[] | string | Promise<MCPContent[] | string>,\n ): this {\n const entry = this.tools.get(name);\n if (entry) {\n entry.handler = handler;\n } else {\n this.tools.set(name, { def: { name }, handler });\n }\n return this;\n }\n\n // ---- Configuration: Resources ----\n\n addResource(def: MCPResourceDefinition, content?: MCPResourceContent): this {\n this.resources.set(def.uri, { def, content });\n return this;\n }\n\n // ---- Configuration: Prompts ----\n\n addPrompt(\n def: MCPPromptDefinition,\n handler?: (args: unknown) => MCPPromptResult | Promise<MCPPromptResult>,\n ): this {\n this.prompts.set(def.name, { def, handler });\n return this;\n }\n\n // ---- Mountable interface ----\n\n async handleRequest(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n pathname: string,\n ): Promise<boolean> {\n // Only handle POST and DELETE to the root of the mount\n if (pathname !== \"/\" && pathname !== \"\") {\n return false;\n }\n if (req.method !== \"POST\" && req.method !== \"DELETE\") {\n return false;\n }\n\n const body = await readBody(req);\n\n // Extract JSON-RPC method for metrics (skip for DELETE — no JSON-RPC body)\n if (this.registry) {\n if (req.method === \"DELETE\") {\n this.registry.incrementCounter(\"aimock_mcp_requests_total\", { method: \"session/delete\" });\n } else {\n try {\n const parsed = JSON.parse(body);\n const method =\n typeof parsed === \"object\" && parsed !== null && \"method\" in parsed\n ? String(parsed.method)\n : \"unknown\";\n this.registry.incrementCounter(\"aimock_mcp_requests_total\", { method });\n } catch {\n this.registry.incrementCounter(\"aimock_mcp_requests_total\", { method: \"unknown\" });\n }\n }\n }\n\n await this.requestHandler(req, res, body);\n\n // Journal the request after the handler completes\n if (this.journal) {\n this.journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"mcp\",\n response: { status: res.statusCode, fixture: null },\n });\n }\n\n return true;\n }\n\n health(): { status: string; [key: string]: unknown } {\n return {\n status: \"ok\",\n tools: this.tools.size,\n resources: this.resources.size,\n prompts: this.prompts.size,\n sessions: this.sessions.size,\n };\n }\n\n setJournal(journal: Journal): void {\n this.journal = journal;\n }\n\n setRegistry(registry: MetricsRegistry): void {\n this.registry = registry;\n }\n\n // ---- Standalone mode ----\n\n async start(): Promise<string> {\n if (this.server) {\n throw new Error(\"Server already started\");\n }\n\n const host = this.options.host ?? \"127.0.0.1\";\n const port = this.options.port ?? 0;\n\n return new Promise((resolve, reject) => {\n const srv = http.createServer((req, res) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => {\n const body = Buffer.concat(chunks).toString();\n\n this.requestHandler(req, res, body)\n .then(() => {\n if (this.journal) {\n this.journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"mcp\",\n response: { status: res.statusCode, fixture: null },\n });\n }\n })\n .catch((err) => {\n console.error(\"MCPMock request error:\", err);\n if (!res.headersSent) {\n res.writeHead(500);\n res.end(\"Internal server error\");\n } else if (!res.writableEnded) {\n res.end();\n }\n });\n });\n });\n\n srv.listen(port, host, () => {\n this.server = srv;\n const addr = srv.address();\n if (typeof addr === \"object\" && addr !== null) {\n resolve(`http://${host}:${addr.port}`);\n } else {\n resolve(`http://${host}:${port}`);\n }\n });\n\n srv.on(\"error\", reject);\n });\n }\n\n async stop(): Promise<void> {\n if (!this.server) {\n throw new Error(\"Server not started\");\n }\n const srv = this.server;\n this.server = null;\n await new Promise<void>((resolve, reject) => {\n srv.close((err) => (err ? reject(err) : resolve()));\n });\n }\n\n // ---- Inspection ----\n\n getRequests(): unknown[] {\n if (!this.journal) return [];\n return this.journal.getAll().filter((e) => e.service === \"mcp\");\n }\n\n getSessions(): Map<string, MCPSession> {\n return new Map(this.sessions);\n }\n\n reset(): this {\n this.tools.clear();\n this.resources.clear();\n this.prompts.clear();\n this.sessions.clear();\n this.requestHandler = this.buildHandler();\n return this;\n }\n\n // ---- Internal ----\n\n private buildHandler() {\n const state: MCPState = {\n serverInfo: this.options.serverInfo ?? { name: \"mcp-mock\", version: \"1.0.0\" },\n tools: this.tools,\n resources: this.resources,\n prompts: this.prompts,\n sessions: this.sessions,\n };\n return createMCPRequestHandler(state);\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-mock.d.cts","names":[],"sources":["../src/mcp-mock.ts"],"sourcesContent":[],"mappings":";;;;;;;cAiBa,OAAA,YAAmB;;EAAnB,QAAA,SAAQ;EAAA,QAAA,OAAA;UAqBG,QAAA;UAOT,MAAA;UAOiB,OAAA;UAAgC,QAAA;UAAR,OAAA;UAarC,cAAA;aAAiC,CAAA,OAAA,CAAA,EA3B5B,cA2B4B;SAQ3C,CAAA,GAAA,EA5BM,iBA4BN,CAAA,EAAA,IAAA;YACwB,CAAA,IAAA,EAAA,MAAA,EAAA,OAAA,EAAA,CAAA,IAAA,EAAA,OAAA,EAAA,GAtBD,UAsBC,EAAA,GAAA,MAAA,GAtBuB,OAsBvB,CAtB+B,UAsB/B,EAAA,GAAA,MAAA,CAAA,CAAA,EAAA,IAAA;aAA0B,CAAA,GAAA,EATxC,qBASwC,EAAA,OAAA,CAAA,EATP,kBASO,CAAA,EAAA,IAAA;WAAR,CAAA,GAAA,EAD1C,mBAC0C,EAAA,OAAA,CAAA,EAAA,CAAA,IAAA,EAAA,OAAA,EAAA,GAAlB,eAAkB,GAAA,OAAA,CAAQ,eAAR,CAAA,CAAA,EAAA,IAAA;eASrC,CAAA,GAAA,EAAL,MAAA,CAAK,eAAA,EAAA,GAAA,EACL,MAAA,CAAK,cADA,EAAA,QAAA,EAAA,MAAA,CAAA,EAGT,OAHS,CAAA,OAAA,CAAA;QACL,CAAA,CAAA,EAAK;IAET,MAAA,EAAA,MAAA;IAwDiB,CAAA,GAAA,EAAA,MAAA,CAAA,EAAA,OAAA;;YAUL,CAAA,OAAA,EAVK,OAUL,CAAA,EAAA,IAAA;aAoDD,CAAA,QAAA,EA1DQ,eA0DR,CAAA,EAAA,IAAA;OAkBa,CAAA,CAAA,EAtEZ,OAsEY,CAAA,MAAA,CAAA;MAAZ,CAAA,CAAA,EAlBD,OAkBC,CAAA,IAAA,CAAA;aA7Me,CAAA,CAAA,EAAA,OAAA,EAAA;EAAS,WAAA,CAAA,CAAA,EA6MxB,GA7MwB,CAAA,MAAA,EA6MZ,UA7MY,CAAA"}
1
+ {"version":3,"file":"mcp-mock.d.cts","names":[],"sources":["../src/mcp-mock.ts"],"sourcesContent":[],"mappings":";;;;;;;cAiBa,OAAA,YAAmB;;EAAnB,QAAA,SAAQ;EAAA,QAAA,OAAA;UAqBG,QAAA;UAOT,MAAA;UAOiB,OAAA;UAAgC,QAAA;UAAR,OAAA;UAarC,cAAA;aAAiC,CAAA,OAAA,CAAA,EA3B5B,cA2B4B;SAQ3C,CAAA,GAAA,EA5BM,iBA4BN,CAAA,EAAA,IAAA;YACwB,CAAA,IAAA,EAAA,MAAA,EAAA,OAAA,EAAA,CAAA,IAAA,EAAA,OAAA,EAAA,GAtBD,UAsBC,EAAA,GAAA,MAAA,GAtBuB,OAsBvB,CAtB+B,UAsB/B,EAAA,GAAA,MAAA,CAAA,CAAA,EAAA,IAAA;aAA0B,CAAA,GAAA,EATxC,qBASwC,EAAA,OAAA,CAAA,EATP,kBASO,CAAA,EAAA,IAAA;WAAR,CAAA,GAAA,EAD1C,mBAC0C,EAAA,OAAA,CAAA,EAAA,CAAA,IAAA,EAAA,OAAA,EAAA,GAAlB,eAAkB,GAAA,OAAA,CAAQ,eAAR,CAAA,CAAA,EAAA,IAAA;eASrC,CAAA,GAAA,EAAL,MAAA,CAAK,eAAA,EAAA,GAAA,EACL,MAAA,CAAK,cADA,EAAA,QAAA,EAAA,MAAA,CAAA,EAGT,OAHS,CAAA,OAAA,CAAA;QACL,CAAA,CAAA,EAAK;IAET,MAAA,EAAA,MAAA;IAwDiB,CAAA,GAAA,EAAA,MAAA,CAAA,EAAA,OAAA;;YAUL,CAAA,OAAA,EAVK,OAUL,CAAA,EAAA,IAAA;aAsDD,CAAA,QAAA,EA5DQ,eA4DR,CAAA,EAAA,IAAA;OAkBa,CAAA,CAAA,EAxEZ,OAwEY,CAAA,MAAA,CAAA;MAAZ,CAAA,CAAA,EAlBD,OAkBC,CAAA,IAAA,CAAA;aA/Me,CAAA,CAAA,EAAA,OAAA,EAAA;EAAS,WAAA,CAAA,CAAA,EA+MxB,GA/MwB,CAAA,MAAA,EA+MZ,UA/MY,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-mock.d.ts","names":[],"sources":["../src/mcp-mock.ts"],"sourcesContent":[],"mappings":";;;;;;;cAiBa,OAAA,YAAmB;;EAAnB,QAAA,SAAQ;EAAA,QAAA,OAAA;UAqBG,QAAA;UAOT,MAAA;UAOiB,OAAA;UAAgC,QAAA;UAAR,OAAA;UAarC,cAAA;aAAiC,CAAA,OAAA,CAAA,EA3B5B,cA2B4B;SAQ3C,CAAA,GAAA,EA5BM,iBA4BN,CAAA,EAAA,IAAA;YACwB,CAAA,IAAA,EAAA,MAAA,EAAA,OAAA,EAAA,CAAA,IAAA,EAAA,OAAA,EAAA,GAtBD,UAsBC,EAAA,GAAA,MAAA,GAtBuB,OAsBvB,CAtB+B,UAsB/B,EAAA,GAAA,MAAA,CAAA,CAAA,EAAA,IAAA;aAA0B,CAAA,GAAA,EATxC,qBASwC,EAAA,OAAA,CAAA,EATP,kBASO,CAAA,EAAA,IAAA;WAAR,CAAA,GAAA,EAD1C,mBAC0C,EAAA,OAAA,CAAA,EAAA,CAAA,IAAA,EAAA,OAAA,EAAA,GAAlB,eAAkB,GAAA,OAAA,CAAQ,eAAR,CAAA,CAAA,EAAA,IAAA;eASrC,CAAA,GAAA,EAAL,MAAA,CAAK,eAAA,EAAA,GAAA,EACL,MAAA,CAAK,cADA,EAAA,QAAA,EAAA,MAAA,CAAA,EAGT,OAHS,CAAA,OAAA,CAAA;QACL,CAAA,CAAA,EAAK;IAET,MAAA,EAAA,MAAA;IAwDiB,CAAA,GAAA,EAAA,MAAA,CAAA,EAAA,OAAA;;YAUL,CAAA,OAAA,EAVK,OAUL,CAAA,EAAA,IAAA;aAoDD,CAAA,QAAA,EA1DQ,eA0DR,CAAA,EAAA,IAAA;OAkBa,CAAA,CAAA,EAtEZ,OAsEY,CAAA,MAAA,CAAA;MAAZ,CAAA,CAAA,EAlBD,OAkBC,CAAA,IAAA,CAAA;aA7Me,CAAA,CAAA,EAAA,OAAA,EAAA;EAAS,WAAA,CAAA,CAAA,EA6MxB,GA7MwB,CAAA,MAAA,EA6MZ,UA7MY,CAAA"}
1
+ {"version":3,"file":"mcp-mock.d.ts","names":[],"sources":["../src/mcp-mock.ts"],"sourcesContent":[],"mappings":";;;;;;;cAiBa,OAAA,YAAmB;;EAAnB,QAAA,SAAQ;EAAA,QAAA,OAAA;UAqBG,QAAA;UAOT,MAAA;UAOiB,OAAA;UAAgC,QAAA;UAAR,OAAA;UAarC,cAAA;aAAiC,CAAA,OAAA,CAAA,EA3B5B,cA2B4B;SAQ3C,CAAA,GAAA,EA5BM,iBA4BN,CAAA,EAAA,IAAA;YACwB,CAAA,IAAA,EAAA,MAAA,EAAA,OAAA,EAAA,CAAA,IAAA,EAAA,OAAA,EAAA,GAtBD,UAsBC,EAAA,GAAA,MAAA,GAtBuB,OAsBvB,CAtB+B,UAsB/B,EAAA,GAAA,MAAA,CAAA,CAAA,EAAA,IAAA;aAA0B,CAAA,GAAA,EATxC,qBASwC,EAAA,OAAA,CAAA,EATP,kBASO,CAAA,EAAA,IAAA;WAAR,CAAA,GAAA,EAD1C,mBAC0C,EAAA,OAAA,CAAA,EAAA,CAAA,IAAA,EAAA,OAAA,EAAA,GAAlB,eAAkB,GAAA,OAAA,CAAQ,eAAR,CAAA,CAAA,EAAA,IAAA;eASrC,CAAA,GAAA,EAAL,MAAA,CAAK,eAAA,EAAA,GAAA,EACL,MAAA,CAAK,cADA,EAAA,QAAA,EAAA,MAAA,CAAA,EAGT,OAHS,CAAA,OAAA,CAAA;QACL,CAAA,CAAA,EAAK;IAET,MAAA,EAAA,MAAA;IAwDiB,CAAA,GAAA,EAAA,MAAA,CAAA,EAAA,OAAA;;YAUL,CAAA,OAAA,EAVK,OAUL,CAAA,EAAA,IAAA;aAsDD,CAAA,QAAA,EA5DQ,eA4DR,CAAA,EAAA,IAAA;OAkBa,CAAA,CAAA,EAxEZ,OAwEY,CAAA,MAAA,CAAA;MAAZ,CAAA,CAAA,EAlBD,OAkBC,CAAA,IAAA,CAAA;aA/Me,CAAA,CAAA,EAAA,OAAA,EAAA;EAAS,WAAA,CAAA,CAAA,EA+MxB,GA/MwB,CAAA,MAAA,EA+MZ,UA/MY,CAAA"}
package/dist/mcp-mock.js CHANGED
@@ -112,7 +112,7 @@ var MCPMock = class {
112
112
  if (!res.headersSent) {
113
113
  res.writeHead(500);
114
114
  res.end("Internal server error");
115
- }
115
+ } else if (!res.writableEnded) res.end();
116
116
  });
117
117
  });
118
118
  });
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-mock.js","names":["http"],"sources":["../src/mcp-mock.ts"],"sourcesContent":["import * as http from \"node:http\";\nimport type { Mountable } from \"./types.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { MetricsRegistry } from \"./metrics.js\";\nimport type {\n MCPMockOptions,\n MCPToolDefinition,\n MCPResourceDefinition,\n MCPResourceContent,\n MCPPromptDefinition,\n MCPPromptResult,\n MCPContent,\n MCPSession,\n} from \"./mcp-types.js\";\nimport { createMCPRequestHandler, type MCPState } from \"./mcp-handler.js\";\nimport { flattenHeaders, readBody } from \"./helpers.js\";\n\nexport class MCPMock implements Mountable {\n private tools: Map<\n string,\n { def: MCPToolDefinition; handler?: (...args: unknown[]) => unknown }\n > = new Map();\n private resources: Map<string, { def: MCPResourceDefinition; content?: MCPResourceContent }> =\n new Map();\n private prompts: Map<\n string,\n {\n def: MCPPromptDefinition;\n handler?: (...args: unknown[]) => MCPPromptResult | Promise<MCPPromptResult>;\n }\n > = new Map();\n private sessions: Map<string, MCPSession> = new Map();\n private server: http.Server | null = null;\n private journal: Journal | null = null;\n private registry: MetricsRegistry | null = null;\n private options: MCPMockOptions;\n private requestHandler: ReturnType<typeof createMCPRequestHandler>;\n\n constructor(options?: MCPMockOptions) {\n this.options = options ?? {};\n this.requestHandler = this.buildHandler();\n }\n\n // ---- Configuration: Tools ----\n\n addTool(def: MCPToolDefinition): this {\n this.tools.set(def.name, { def });\n return this;\n }\n\n onToolCall(\n name: string,\n handler: (args: unknown) => MCPContent[] | string | Promise<MCPContent[] | string>,\n ): this {\n const entry = this.tools.get(name);\n if (entry) {\n entry.handler = handler;\n } else {\n this.tools.set(name, { def: { name }, handler });\n }\n return this;\n }\n\n // ---- Configuration: Resources ----\n\n addResource(def: MCPResourceDefinition, content?: MCPResourceContent): this {\n this.resources.set(def.uri, { def, content });\n return this;\n }\n\n // ---- Configuration: Prompts ----\n\n addPrompt(\n def: MCPPromptDefinition,\n handler?: (args: unknown) => MCPPromptResult | Promise<MCPPromptResult>,\n ): this {\n this.prompts.set(def.name, { def, handler });\n return this;\n }\n\n // ---- Mountable interface ----\n\n async handleRequest(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n pathname: string,\n ): Promise<boolean> {\n // Only handle POST and DELETE to the root of the mount\n if (pathname !== \"/\" && pathname !== \"\") {\n return false;\n }\n if (req.method !== \"POST\" && req.method !== \"DELETE\") {\n return false;\n }\n\n const body = await readBody(req);\n\n // Extract JSON-RPC method for metrics (skip for DELETE — no JSON-RPC body)\n if (this.registry) {\n if (req.method === \"DELETE\") {\n this.registry.incrementCounter(\"aimock_mcp_requests_total\", { method: \"session/delete\" });\n } else {\n try {\n const parsed = JSON.parse(body);\n const method =\n typeof parsed === \"object\" && parsed !== null && \"method\" in parsed\n ? String(parsed.method)\n : \"unknown\";\n this.registry.incrementCounter(\"aimock_mcp_requests_total\", { method });\n } catch {\n this.registry.incrementCounter(\"aimock_mcp_requests_total\", { method: \"unknown\" });\n }\n }\n }\n\n await this.requestHandler(req, res, body);\n\n // Journal the request after the handler completes\n if (this.journal) {\n this.journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"mcp\",\n response: { status: res.statusCode, fixture: null },\n });\n }\n\n return true;\n }\n\n health(): { status: string; [key: string]: unknown } {\n return {\n status: \"ok\",\n tools: this.tools.size,\n resources: this.resources.size,\n prompts: this.prompts.size,\n sessions: this.sessions.size,\n };\n }\n\n setJournal(journal: Journal): void {\n this.journal = journal;\n }\n\n setRegistry(registry: MetricsRegistry): void {\n this.registry = registry;\n }\n\n // ---- Standalone mode ----\n\n async start(): Promise<string> {\n if (this.server) {\n throw new Error(\"Server already started\");\n }\n\n const host = this.options.host ?? \"127.0.0.1\";\n const port = this.options.port ?? 0;\n\n return new Promise((resolve, reject) => {\n const srv = http.createServer((req, res) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => {\n const body = Buffer.concat(chunks).toString();\n\n this.requestHandler(req, res, body)\n .then(() => {\n if (this.journal) {\n this.journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"mcp\",\n response: { status: res.statusCode, fixture: null },\n });\n }\n })\n .catch((err) => {\n console.error(\"MCPMock request error:\", err);\n if (!res.headersSent) {\n res.writeHead(500);\n res.end(\"Internal server error\");\n }\n });\n });\n });\n\n srv.listen(port, host, () => {\n this.server = srv;\n const addr = srv.address();\n if (typeof addr === \"object\" && addr !== null) {\n resolve(`http://${host}:${addr.port}`);\n } else {\n resolve(`http://${host}:${port}`);\n }\n });\n\n srv.on(\"error\", reject);\n });\n }\n\n async stop(): Promise<void> {\n if (!this.server) {\n throw new Error(\"Server not started\");\n }\n const srv = this.server;\n this.server = null;\n await new Promise<void>((resolve, reject) => {\n srv.close((err) => (err ? reject(err) : resolve()));\n });\n }\n\n // ---- Inspection ----\n\n getRequests(): unknown[] {\n if (!this.journal) return [];\n return this.journal.getAll().filter((e) => e.service === \"mcp\");\n }\n\n getSessions(): Map<string, MCPSession> {\n return new Map(this.sessions);\n }\n\n reset(): this {\n this.tools.clear();\n this.resources.clear();\n this.prompts.clear();\n this.sessions.clear();\n this.requestHandler = this.buildHandler();\n return this;\n }\n\n // ---- Internal ----\n\n private buildHandler() {\n const state: MCPState = {\n serverInfo: this.options.serverInfo ?? { name: \"mcp-mock\", version: \"1.0.0\" },\n tools: this.tools,\n resources: this.resources,\n prompts: this.prompts,\n sessions: this.sessions,\n };\n return createMCPRequestHandler(state);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"mcp-mock.js","names":["http"],"sources":["../src/mcp-mock.ts"],"sourcesContent":["import * as http from \"node:http\";\nimport type { Mountable } from \"./types.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { MetricsRegistry } from \"./metrics.js\";\nimport type {\n MCPMockOptions,\n MCPToolDefinition,\n MCPResourceDefinition,\n MCPResourceContent,\n MCPPromptDefinition,\n MCPPromptResult,\n MCPContent,\n MCPSession,\n} from \"./mcp-types.js\";\nimport { createMCPRequestHandler, type MCPState } from \"./mcp-handler.js\";\nimport { flattenHeaders, readBody } from \"./helpers.js\";\n\nexport class MCPMock implements Mountable {\n private tools: Map<\n string,\n { def: MCPToolDefinition; handler?: (...args: unknown[]) => unknown }\n > = new Map();\n private resources: Map<string, { def: MCPResourceDefinition; content?: MCPResourceContent }> =\n new Map();\n private prompts: Map<\n string,\n {\n def: MCPPromptDefinition;\n handler?: (...args: unknown[]) => MCPPromptResult | Promise<MCPPromptResult>;\n }\n > = new Map();\n private sessions: Map<string, MCPSession> = new Map();\n private server: http.Server | null = null;\n private journal: Journal | null = null;\n private registry: MetricsRegistry | null = null;\n private options: MCPMockOptions;\n private requestHandler: ReturnType<typeof createMCPRequestHandler>;\n\n constructor(options?: MCPMockOptions) {\n this.options = options ?? {};\n this.requestHandler = this.buildHandler();\n }\n\n // ---- Configuration: Tools ----\n\n addTool(def: MCPToolDefinition): this {\n this.tools.set(def.name, { def });\n return this;\n }\n\n onToolCall(\n name: string,\n handler: (args: unknown) => MCPContent[] | string | Promise<MCPContent[] | string>,\n ): this {\n const entry = this.tools.get(name);\n if (entry) {\n entry.handler = handler;\n } else {\n this.tools.set(name, { def: { name }, handler });\n }\n return this;\n }\n\n // ---- Configuration: Resources ----\n\n addResource(def: MCPResourceDefinition, content?: MCPResourceContent): this {\n this.resources.set(def.uri, { def, content });\n return this;\n }\n\n // ---- Configuration: Prompts ----\n\n addPrompt(\n def: MCPPromptDefinition,\n handler?: (args: unknown) => MCPPromptResult | Promise<MCPPromptResult>,\n ): this {\n this.prompts.set(def.name, { def, handler });\n return this;\n }\n\n // ---- Mountable interface ----\n\n async handleRequest(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n pathname: string,\n ): Promise<boolean> {\n // Only handle POST and DELETE to the root of the mount\n if (pathname !== \"/\" && pathname !== \"\") {\n return false;\n }\n if (req.method !== \"POST\" && req.method !== \"DELETE\") {\n return false;\n }\n\n const body = await readBody(req);\n\n // Extract JSON-RPC method for metrics (skip for DELETE — no JSON-RPC body)\n if (this.registry) {\n if (req.method === \"DELETE\") {\n this.registry.incrementCounter(\"aimock_mcp_requests_total\", { method: \"session/delete\" });\n } else {\n try {\n const parsed = JSON.parse(body);\n const method =\n typeof parsed === \"object\" && parsed !== null && \"method\" in parsed\n ? String(parsed.method)\n : \"unknown\";\n this.registry.incrementCounter(\"aimock_mcp_requests_total\", { method });\n } catch {\n this.registry.incrementCounter(\"aimock_mcp_requests_total\", { method: \"unknown\" });\n }\n }\n }\n\n await this.requestHandler(req, res, body);\n\n // Journal the request after the handler completes\n if (this.journal) {\n this.journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"mcp\",\n response: { status: res.statusCode, fixture: null },\n });\n }\n\n return true;\n }\n\n health(): { status: string; [key: string]: unknown } {\n return {\n status: \"ok\",\n tools: this.tools.size,\n resources: this.resources.size,\n prompts: this.prompts.size,\n sessions: this.sessions.size,\n };\n }\n\n setJournal(journal: Journal): void {\n this.journal = journal;\n }\n\n setRegistry(registry: MetricsRegistry): void {\n this.registry = registry;\n }\n\n // ---- Standalone mode ----\n\n async start(): Promise<string> {\n if (this.server) {\n throw new Error(\"Server already started\");\n }\n\n const host = this.options.host ?? \"127.0.0.1\";\n const port = this.options.port ?? 0;\n\n return new Promise((resolve, reject) => {\n const srv = http.createServer((req, res) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => {\n const body = Buffer.concat(chunks).toString();\n\n this.requestHandler(req, res, body)\n .then(() => {\n if (this.journal) {\n this.journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"mcp\",\n response: { status: res.statusCode, fixture: null },\n });\n }\n })\n .catch((err) => {\n console.error(\"MCPMock request error:\", err);\n if (!res.headersSent) {\n res.writeHead(500);\n res.end(\"Internal server error\");\n } else if (!res.writableEnded) {\n res.end();\n }\n });\n });\n });\n\n srv.listen(port, host, () => {\n this.server = srv;\n const addr = srv.address();\n if (typeof addr === \"object\" && addr !== null) {\n resolve(`http://${host}:${addr.port}`);\n } else {\n resolve(`http://${host}:${port}`);\n }\n });\n\n srv.on(\"error\", reject);\n });\n }\n\n async stop(): Promise<void> {\n if (!this.server) {\n throw new Error(\"Server not started\");\n }\n const srv = this.server;\n this.server = null;\n await new Promise<void>((resolve, reject) => {\n srv.close((err) => (err ? reject(err) : resolve()));\n });\n }\n\n // ---- Inspection ----\n\n getRequests(): unknown[] {\n if (!this.journal) return [];\n return this.journal.getAll().filter((e) => e.service === \"mcp\");\n }\n\n getSessions(): Map<string, MCPSession> {\n return new Map(this.sessions);\n }\n\n reset(): this {\n this.tools.clear();\n this.resources.clear();\n this.prompts.clear();\n this.sessions.clear();\n this.requestHandler = this.buildHandler();\n return this;\n }\n\n // ---- Internal ----\n\n private buildHandler() {\n const state: MCPState = {\n serverInfo: this.options.serverInfo ?? { name: \"mcp-mock\", version: \"1.0.0\" },\n tools: this.tools,\n resources: this.resources,\n prompts: this.prompts,\n sessions: this.sessions,\n };\n return createMCPRequestHandler(state);\n }\n}\n"],"mappings":"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"}
package/dist/messages.cjs CHANGED
@@ -496,7 +496,8 @@ async function handleMessages(req, res, raw, fixtures, journal, defaults, setCor
496
496
  let claudeReq;
497
497
  try {
498
498
  claudeReq = JSON.parse(raw);
499
- } catch {
499
+ } catch (parseErr) {
500
+ const detail = parseErr instanceof Error ? parseErr.message : "unknown";
500
501
  journal.add({
501
502
  method: req.method ?? "POST",
502
503
  path: req.url ?? "/v1/messages",
@@ -508,7 +509,7 @@ async function handleMessages(req, res, raw, fixtures, journal, defaults, setCor
508
509
  }
509
510
  });
510
511
  require_sse_writer.writeErrorResponse(res, 400, JSON.stringify({ error: {
511
- message: "Malformed JSON",
512
+ message: `Malformed JSON: ${detail}`,
512
513
  type: "invalid_request_error"
513
514
  } }));
514
515
  return;
@@ -548,9 +549,10 @@ async function handleMessages(req, res, raw, fixtures, journal, defaults, setCor
548
549
  return;
549
550
  }
550
551
  }
551
- const strictStatus = defaults.strict ? 503 : 404;
552
- const strictMessage = defaults.strict ? "Strict mode: no fixture matched" : "No fixture matched";
553
- if (defaults.strict) logger.error(`STRICT: No fixture matched for ${req.method ?? "POST"} ${req.url ?? "/v1/messages"}`);
552
+ const effectiveStrict = require_helpers.resolveStrictMode(defaults.strict, req.headers);
553
+ const strictStatus = effectiveStrict ? 503 : 404;
554
+ const strictMessage = effectiveStrict ? "Strict mode: no fixture matched" : "No fixture matched";
555
+ if (effectiveStrict) logger.error(`STRICT: No fixture matched for ${req.method ?? "POST"} ${req.url ?? "/v1/messages"}`);
554
556
  journal.add({
555
557
  method: req.method ?? "POST",
556
558
  path: req.url ?? "/v1/messages",
@@ -558,7 +560,8 @@ async function handleMessages(req, res, raw, fixtures, journal, defaults, setCor
558
560
  body: completionReq,
559
561
  response: {
560
562
  status: strictStatus,
561
- fixture: null
563
+ fixture: null,
564
+ ...require_helpers.strictOverrideField(defaults.strict, req.headers)
562
565
  }
563
566
  });
564
567
  require_sse_writer.writeErrorResponse(res, strictStatus, JSON.stringify({ error: {