@copilotkit/aimock 1.37.4 → 1.39.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 (175) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +48 -0
  4. package/README.md +50 -11
  5. package/dist/agui-handler.cjs +20 -18
  6. package/dist/agui-handler.cjs.map +1 -1
  7. package/dist/agui-handler.d.cts +8 -1
  8. package/dist/agui-handler.d.cts.map +1 -1
  9. package/dist/agui-handler.d.ts +8 -1
  10. package/dist/agui-handler.d.ts.map +1 -1
  11. package/dist/agui-handler.js +20 -18
  12. package/dist/agui-handler.js.map +1 -1
  13. package/dist/agui-recorder.cjs +6 -0
  14. package/dist/agui-recorder.cjs.map +1 -1
  15. package/dist/agui-recorder.d.cts.map +1 -1
  16. package/dist/agui-recorder.d.ts.map +1 -1
  17. package/dist/agui-recorder.js +6 -0
  18. package/dist/agui-recorder.js.map +1 -1
  19. package/dist/agui-stub.d.cts +2 -2
  20. package/dist/agui-stub.d.ts +2 -2
  21. package/dist/agui-types.d.cts +19 -1
  22. package/dist/agui-types.d.cts.map +1 -1
  23. package/dist/agui-types.d.ts +19 -1
  24. package/dist/agui-types.d.ts.map +1 -1
  25. package/dist/aimock-cli.cjs +1 -1
  26. package/dist/aimock-cli.cjs.map +1 -1
  27. package/dist/aimock-cli.js +1 -1
  28. package/dist/aimock-cli.js.map +1 -1
  29. package/dist/api-key-auth.cjs +166 -0
  30. package/dist/api-key-auth.cjs.map +1 -0
  31. package/dist/api-key-auth.d.cts +17 -0
  32. package/dist/api-key-auth.d.cts.map +1 -0
  33. package/dist/api-key-auth.d.ts +17 -0
  34. package/dist/api-key-auth.d.ts.map +1 -0
  35. package/dist/api-key-auth.js +158 -0
  36. package/dist/api-key-auth.js.map +1 -0
  37. package/dist/cli.cjs +14 -11
  38. package/dist/cli.cjs.map +1 -1
  39. package/dist/cli.js +14 -11
  40. package/dist/cli.js.map +1 -1
  41. package/dist/config-loader.cjs +4 -2
  42. package/dist/config-loader.cjs.map +1 -1
  43. package/dist/config-loader.d.cts +2 -1
  44. package/dist/config-loader.d.cts.map +1 -1
  45. package/dist/config-loader.d.ts +2 -1
  46. package/dist/config-loader.d.ts.map +1 -1
  47. package/dist/config-loader.js +5 -3
  48. package/dist/config-loader.js.map +1 -1
  49. package/dist/fal-audio.cjs +2 -0
  50. package/dist/fal-audio.cjs.map +1 -1
  51. package/dist/fal-audio.d.cts.map +1 -1
  52. package/dist/fal-audio.d.ts.map +1 -1
  53. package/dist/fal-audio.js +2 -0
  54. package/dist/fal-audio.js.map +1 -1
  55. package/dist/fal.cjs +4 -1
  56. package/dist/fal.cjs.map +1 -1
  57. package/dist/fal.d.cts.map +1 -1
  58. package/dist/fal.d.ts.map +1 -1
  59. package/dist/fal.js +4 -1
  60. package/dist/fal.js.map +1 -1
  61. package/dist/fixture-loader.cjs +0 -0
  62. package/dist/fixture-loader.cjs.map +1 -1
  63. package/dist/fixture-loader.d.cts +3 -1
  64. package/dist/fixture-loader.d.cts.map +1 -1
  65. package/dist/fixture-loader.d.ts +3 -1
  66. package/dist/fixture-loader.d.ts.map +1 -1
  67. package/dist/fixture-loader.js +0 -0
  68. package/dist/fixture-loader.js.map +1 -1
  69. package/dist/grok-video.cjs +9 -2
  70. package/dist/grok-video.cjs.map +1 -1
  71. package/dist/grok-video.js +10 -3
  72. package/dist/grok-video.js.map +1 -1
  73. package/dist/helpers.cjs +16 -22
  74. package/dist/helpers.cjs.map +1 -1
  75. package/dist/helpers.d.cts.map +1 -1
  76. package/dist/helpers.d.ts.map +1 -1
  77. package/dist/helpers.js +16 -21
  78. package/dist/helpers.js.map +1 -1
  79. package/dist/index.d.cts +3 -3
  80. package/dist/index.d.ts +3 -3
  81. package/dist/llmock.cjs +37 -12
  82. package/dist/llmock.cjs.map +1 -1
  83. package/dist/llmock.d.cts +26 -2
  84. package/dist/llmock.d.cts.map +1 -1
  85. package/dist/llmock.d.ts +26 -2
  86. package/dist/llmock.d.ts.map +1 -1
  87. package/dist/llmock.js +39 -15
  88. package/dist/llmock.js.map +1 -1
  89. package/dist/openrouter-chat.cjs +333 -0
  90. package/dist/openrouter-chat.cjs.map +1 -0
  91. package/dist/openrouter-chat.js +325 -0
  92. package/dist/openrouter-chat.js.map +1 -0
  93. package/dist/openrouter-video.cjs +24 -7
  94. package/dist/openrouter-video.cjs.map +1 -1
  95. package/dist/openrouter-video.d.cts.map +1 -1
  96. package/dist/openrouter-video.d.ts.map +1 -1
  97. package/dist/openrouter-video.js +25 -8
  98. package/dist/openrouter-video.js.map +1 -1
  99. package/dist/provider-auth.cjs +20 -0
  100. package/dist/provider-auth.cjs.map +1 -1
  101. package/dist/provider-auth.js +20 -1
  102. package/dist/provider-auth.js.map +1 -1
  103. package/dist/recorder.cjs +50 -8
  104. package/dist/recorder.cjs.map +1 -1
  105. package/dist/recorder.d.cts.map +1 -1
  106. package/dist/recorder.d.ts.map +1 -1
  107. package/dist/recorder.js +50 -10
  108. package/dist/recorder.js.map +1 -1
  109. package/dist/responses.cjs +200 -89
  110. package/dist/responses.cjs.map +1 -1
  111. package/dist/responses.d.cts +18 -3
  112. package/dist/responses.d.cts.map +1 -1
  113. package/dist/responses.d.ts +18 -3
  114. package/dist/responses.d.ts.map +1 -1
  115. package/dist/responses.js +199 -90
  116. package/dist/responses.js.map +1 -1
  117. package/dist/server.cjs +190 -83
  118. package/dist/server.cjs.map +1 -1
  119. package/dist/server.d.cts +7 -2
  120. package/dist/server.d.cts.map +1 -1
  121. package/dist/server.d.ts +7 -2
  122. package/dist/server.d.ts.map +1 -1
  123. package/dist/server.js +190 -85
  124. package/dist/server.js.map +1 -1
  125. package/dist/sse-writer.cjs +3 -0
  126. package/dist/sse-writer.cjs.map +1 -1
  127. package/dist/sse-writer.d.cts +8 -0
  128. package/dist/sse-writer.d.cts.map +1 -1
  129. package/dist/sse-writer.d.ts +8 -0
  130. package/dist/sse-writer.d.ts.map +1 -1
  131. package/dist/sse-writer.js +3 -0
  132. package/dist/sse-writer.js.map +1 -1
  133. package/dist/stream-collapse.cjs +103 -0
  134. package/dist/stream-collapse.cjs.map +1 -1
  135. package/dist/stream-collapse.d.cts +7 -0
  136. package/dist/stream-collapse.d.cts.map +1 -1
  137. package/dist/stream-collapse.d.ts +7 -0
  138. package/dist/stream-collapse.d.ts.map +1 -1
  139. package/dist/stream-collapse.js +103 -0
  140. package/dist/stream-collapse.js.map +1 -1
  141. package/dist/transcription.cjs +49 -2
  142. package/dist/transcription.cjs.map +1 -1
  143. package/dist/transcription.d.cts.map +1 -1
  144. package/dist/transcription.d.ts.map +1 -1
  145. package/dist/transcription.js +50 -3
  146. package/dist/transcription.js.map +1 -1
  147. package/dist/types.d.cts +187 -5
  148. package/dist/types.d.cts.map +1 -1
  149. package/dist/types.d.ts +187 -5
  150. package/dist/types.d.ts.map +1 -1
  151. package/dist/vector-types.d.ts.map +1 -1
  152. package/dist/veo-video.cjs +9 -2
  153. package/dist/veo-video.cjs.map +1 -1
  154. package/dist/veo-video.js +10 -3
  155. package/dist/veo-video.js.map +1 -1
  156. package/dist/ws-gemini-live.cjs +41 -6
  157. package/dist/ws-gemini-live.cjs.map +1 -1
  158. package/dist/ws-gemini-live.js +41 -6
  159. package/dist/ws-gemini-live.js.map +1 -1
  160. package/dist/ws-realtime.cjs +306 -57
  161. package/dist/ws-realtime.cjs.map +1 -1
  162. package/dist/ws-realtime.d.cts +1 -0
  163. package/dist/ws-realtime.d.cts.map +1 -1
  164. package/dist/ws-realtime.d.ts +1 -0
  165. package/dist/ws-realtime.d.ts.map +1 -1
  166. package/dist/ws-realtime.js +307 -58
  167. package/dist/ws-realtime.js.map +1 -1
  168. package/dist/ws-responses.cjs +11 -6
  169. package/dist/ws-responses.cjs.map +1 -1
  170. package/dist/ws-responses.d.cts.map +1 -1
  171. package/dist/ws-responses.d.ts.map +1 -1
  172. package/dist/ws-responses.js +12 -7
  173. package/dist/ws-responses.js.map +1 -1
  174. package/package.json +3 -2
  175. package/skills/write-fixtures/SKILL.md +53 -19
@@ -1 +1 @@
1
- {"version":3,"file":"ws-responses.cjs","names":["responsesToCompletionRequest","DEFAULT_TEST_ID","matchFixtureDiagnostic","resolveStrictMode","strictNoMatchMessage","strictNoMatchLogLine","flattenHeaders","strictOverrideField","resolveResponse","isErrorResponse","isContentWithToolCallsResponse","buildContentWithToolCallsStreamEvents","resolveReasoningForModel","extractOverrides","createInterruptionSignal","isTextResponse","buildTextStreamEvents","isToolCallResponse","buildToolCallStreamEvents","calculateDelay","delay"],"sources":["../src/ws-responses.ts"],"sourcesContent":["/**\n * WebSocket handler for OpenAI Responses API.\n *\n * Accepts `{ type: \"response.create\", model: \"...\", input: [...] }` messages over\n * WebSocket and sends back the same Responses API SSE events as the HTTP\n * handler, but as individual WebSocket text frames.\n */\n\nimport type { ChatCompletionRequest, Fixture } from \"./types.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport {\n responsesToCompletionRequest,\n buildTextStreamEvents,\n buildToolCallStreamEvents,\n buildContentWithToolCallsStreamEvents,\n type ResponsesSSEEvent,\n} from \"./responses.js\";\nimport {\n isTextResponse,\n isToolCallResponse,\n isContentWithToolCallsResponse,\n isErrorResponse,\n extractOverrides,\n resolveResponse,\n resolveStrictMode,\n resolveReasoningForModel,\n strictOverrideField,\n flattenHeaders,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { createInterruptionSignal } from \"./interruption.js\";\nimport { delay, calculateDelay } from \"./sse-writer.js\";\nimport { DEFAULT_TEST_ID, type Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\nimport type { WebSocketConnection } from \"./ws-framing.js\";\n\ninterface ResponseCreateMessage {\n type: \"response.create\";\n model?: string;\n input?: unknown[];\n instructions?: string;\n tools?: unknown[];\n tool_choice?: string | object;\n stream?: boolean;\n temperature?: number;\n max_output_tokens?: number;\n [key: string]: unknown;\n}\n\nfunction isResponseCreateMessage(msg: unknown): msg is ResponseCreateMessage {\n return (\n typeof msg === \"object\" &&\n msg !== null &&\n (msg as ResponseCreateMessage).type === \"response.create\"\n );\n}\n\nfunction buildErrorEvent(\n message: string,\n type = \"invalid_request_error\",\n code?: string,\n): ResponsesSSEEvent {\n return {\n type: \"error\",\n error: { message, type, code },\n };\n}\n\nexport function handleWebSocketResponses(\n ws: WebSocketConnection,\n fixtures: Fixture[],\n journal: Journal,\n defaults: {\n latency: number;\n chunkSize: number;\n replaySpeed?: number;\n model: string;\n logger: Logger;\n strict?: boolean;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n testId?: string;\n upgradeHeaders?: import(\"node:http\").IncomingHttpHeaders;\n },\n): void {\n const { logger } = defaults;\n // Serialize message processing to prevent event interleaving\n let pending = Promise.resolve();\n ws.on(\"message\", (raw: string) => {\n pending = pending.then(() =>\n processMessage(raw, ws, fixtures, journal, defaults).catch((err: unknown) => {\n const msg = err instanceof Error ? err.message : \"Internal error\";\n logger.error(`WebSocket responses error: ${msg}`);\n try {\n ws.send(JSON.stringify(buildErrorEvent(msg, \"server_error\")));\n } catch (sendErr) {\n defaults.logger.debug(\n `Failed to send error to client: ${sendErr instanceof Error ? sendErr.message : \"unknown\"}`,\n );\n }\n }),\n );\n });\n}\n\nasync function processMessage(\n raw: string,\n ws: WebSocketConnection,\n fixtures: Fixture[],\n journal: Journal,\n defaults: {\n latency: number;\n chunkSize: number;\n replaySpeed?: number;\n model: string;\n logger: Logger;\n strict?: boolean;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n testId?: string;\n upgradeHeaders?: import(\"node:http\").IncomingHttpHeaders;\n },\n): Promise<void> {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n ws.send(\n JSON.stringify(\n buildErrorEvent(`Malformed JSON: ${detail}`, \"invalid_request_error\", \"invalid_json\"),\n ),\n );\n return;\n }\n\n if (!isResponseCreateMessage(parsed)) {\n ws.send(\n JSON.stringify(\n buildErrorEvent(\n 'Expected message type \"response.create\"',\n \"invalid_request_error\",\n \"invalid_message_type\",\n ),\n ),\n );\n return;\n }\n\n const responsesReq = {\n model: parsed.model ?? defaults.model,\n input: (parsed.input ?? []) as {\n role?: string;\n type?: string;\n content?: string | { type: string; text?: string }[];\n call_id?: string;\n name?: string;\n arguments?: string;\n output?: string;\n id?: string;\n }[],\n instructions: parsed.instructions,\n tools: parsed.tools as\n | {\n type: \"function\";\n name: string;\n description?: string;\n parameters?: object;\n strict?: boolean;\n }[]\n | undefined,\n tool_choice: parsed.tool_choice,\n stream: parsed.stream,\n temperature: parsed.temperature,\n max_output_tokens: parsed.max_output_tokens,\n };\n\n const completionReq = responsesToCompletionRequest(responsesReq);\n completionReq._endpointType = \"chat\";\n const contextHeader = defaults.upgradeHeaders?.[\"x-aimock-context\"];\n completionReq._context =\n typeof contextHeader === \"string\"\n ? contextHeader\n : Array.isArray(contextHeader) && contextHeader.length > 0\n ? contextHeader[0]\n : undefined;\n const testId = defaults.testId ?? DEFAULT_TEST_ID;\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n completionReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n }\n\n if (!fixture) {\n if (resolveStrictMode(defaults.strict, defaults.upgradeHeaders)) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n defaults.logger.error(strictNoMatchLogLine(\"WS\", \"/v1/responses\", skippedBySequenceOrTurn));\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, defaults.upgradeHeaders),\n },\n });\n ws.close(1008, strictMessage);\n return;\n }\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: {\n status: 404,\n fixture: null,\n ...strictOverrideField(defaults.strict, defaults.upgradeHeaders),\n },\n });\n ws.send(\n JSON.stringify(\n buildErrorEvent(\"No fixture matched\", \"invalid_request_error\", \"no_fixture_match\"),\n ),\n );\n return;\n }\n\n const response = await resolveResponse(fixture, completionReq);\n const latency = fixture.latency ?? defaults.latency;\n const chunkSize = Math.max(1, fixture.chunkSize ?? defaults.chunkSize);\n\n // The WS path has no per-request `req.headers`; strict is resolved from the\n // connection's upgrade headers (see the `!fixture` branch above). Used below\n // to gate the synthesized reasoning channel on the requested model's capability.\n const effectiveStrict = resolveStrictMode(defaults.strict, defaults.upgradeHeaders);\n\n // Error response\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status, fixture },\n });\n ws.send(\n JSON.stringify(\n buildErrorEvent(response.error.message, response.error.type, response.error.code),\n ),\n );\n return;\n }\n\n // Content + tool calls response (must be checked before isTextResponse / isToolCallResponse)\n if (isContentWithToolCallsResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n const events = buildContentWithToolCallsStreamEvents(\n response.content ?? \"\",\n response.toolCalls ?? [],\n completionReq.model,\n chunkSize,\n resolveReasoningForModel(\n response.reasoning,\n completionReq.model,\n effectiveStrict,\n defaults.logger,\n ),\n response.webSearches,\n extractOverrides(response),\n response.blocks,\n );\n\n const interruption = createInterruptionSignal(fixture);\n const completed = await sendEvents(\n ws,\n events,\n latency,\n interruption?.signal,\n interruption?.tick,\n fixture.recordedTimings,\n fixture.replaySpeed ?? defaults.replaySpeed,\n );\n if (!completed) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n }\n interruption?.cleanup();\n return;\n }\n\n // Text response\n if (isTextResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n const events = buildTextStreamEvents(\n response.content,\n completionReq.model,\n chunkSize,\n resolveReasoningForModel(\n response.reasoning,\n completionReq.model,\n effectiveStrict,\n defaults.logger,\n ),\n response.webSearches,\n extractOverrides(response),\n );\n const interruption = createInterruptionSignal(fixture);\n const completed = await sendEvents(\n ws,\n events,\n latency,\n interruption?.signal,\n interruption?.tick,\n fixture.recordedTimings,\n fixture.replaySpeed ?? defaults.replaySpeed,\n );\n if (!completed) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n }\n interruption?.cleanup();\n return;\n }\n\n // Tool call response\n if (isToolCallResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 200, fixture },\n });\n const events = buildToolCallStreamEvents(\n response.toolCalls,\n completionReq.model,\n chunkSize,\n // Gate the synthesized reasoning channel on the requested model's\n // capability, matching the WS text / content+tool branches and the HTTP\n // tool-only path so reasoning emission is transport-independent.\n resolveReasoningForModel(\n response.reasoning,\n completionReq.model,\n effectiveStrict,\n defaults.logger,\n ),\n response.webSearches,\n extractOverrides(response),\n );\n const interruption = createInterruptionSignal(fixture);\n const completed = await sendEvents(\n ws,\n events,\n latency,\n interruption?.signal,\n interruption?.tick,\n fixture.recordedTimings,\n fixture.replaySpeed ?? defaults.replaySpeed,\n );\n if (!completed) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n }\n interruption?.cleanup();\n return;\n }\n\n // Unknown response type\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 500, fixture },\n });\n ws.send(\n JSON.stringify(\n buildErrorEvent(\"Fixture response did not match any known type\", \"server_error\"),\n ),\n );\n}\n\nasync function sendEvents(\n ws: WebSocketConnection,\n events: ResponsesSSEEvent[],\n latency: number,\n signal?: AbortSignal,\n onChunkSent?: () => void,\n recordedTimings?: import(\"./types.js\").RecordedTimings,\n replaySpeed?: number,\n): Promise<boolean> {\n let eventIndex = 0;\n for (const event of events) {\n if (ws.isClosed) return false;\n const chunkDelay = calculateDelay(eventIndex, undefined, latency, recordedTimings, replaySpeed);\n if (chunkDelay > 0) await delay(chunkDelay, signal);\n if (signal?.aborted) return false;\n if (ws.isClosed) return false;\n ws.send(JSON.stringify(event));\n eventIndex++;\n onChunkSent?.();\n if (signal?.aborted) return false;\n }\n return true;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ws-responses.cjs","names":["requestWantsEncryptedReasoning","requestIncludesEncryptedReasoning","responsesToCompletionRequest","DEFAULT_TEST_ID","matchFixtureDiagnostic","resolveStrictMode","strictNoMatchMessage","strictNoMatchLogLine","flattenHeaders","strictOverrideField","resolveResponse","isErrorResponse","isContentWithToolCallsResponse","buildContentWithToolCallsStreamEvents","resolveReasoningForModel","extractOverrides","createInterruptionSignal","isTextResponse","buildTextStreamEvents","isToolCallResponse","buildToolCallStreamEvents","calculateDelay","delay"],"sources":["../src/ws-responses.ts"],"sourcesContent":["/**\n * WebSocket handler for OpenAI Responses API.\n *\n * Accepts `{ type: \"response.create\", model: \"...\", input: [...] }` messages over\n * WebSocket and sends back the same Responses API SSE events as the HTTP\n * handler, but as individual WebSocket text frames.\n */\n\nimport type { ChatCompletionRequest, Fixture } from \"./types.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport {\n responsesToCompletionRequest,\n requestIncludesEncryptedReasoning,\n requestWantsEncryptedReasoning,\n buildTextStreamEvents,\n buildToolCallStreamEvents,\n buildContentWithToolCallsStreamEvents,\n type ResponsesSSEEvent,\n} from \"./responses.js\";\nimport {\n isTextResponse,\n isToolCallResponse,\n isContentWithToolCallsResponse,\n isErrorResponse,\n extractOverrides,\n resolveResponse,\n resolveStrictMode,\n resolveReasoningForModel,\n strictOverrideField,\n flattenHeaders,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { createInterruptionSignal } from \"./interruption.js\";\nimport { delay, calculateDelay } from \"./sse-writer.js\";\nimport { DEFAULT_TEST_ID, type Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\nimport type { WebSocketConnection } from \"./ws-framing.js\";\n\ninterface ResponseCreateMessage {\n type: \"response.create\";\n model?: string;\n input?: unknown[];\n instructions?: string;\n tools?: unknown[];\n tool_choice?: string | object;\n stream?: boolean;\n temperature?: number;\n max_output_tokens?: number;\n [key: string]: unknown;\n}\n\nfunction isResponseCreateMessage(msg: unknown): msg is ResponseCreateMessage {\n return (\n typeof msg === \"object\" &&\n msg !== null &&\n (msg as ResponseCreateMessage).type === \"response.create\"\n );\n}\n\nfunction buildErrorEvent(\n message: string,\n type = \"invalid_request_error\",\n code?: string,\n): ResponsesSSEEvent {\n return {\n type: \"error\",\n error: { message, type, code },\n };\n}\n\nexport function handleWebSocketResponses(\n ws: WebSocketConnection,\n fixtures: Fixture[],\n journal: Journal,\n defaults: {\n latency: number;\n chunkSize: number;\n replaySpeed?: number;\n model: string;\n logger: Logger;\n strict?: boolean;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n testId?: string;\n upgradeHeaders?: import(\"node:http\").IncomingHttpHeaders;\n },\n): void {\n const { logger } = defaults;\n // Serialize message processing to prevent event interleaving\n let pending = Promise.resolve();\n ws.on(\"message\", (raw: string) => {\n pending = pending.then(() =>\n processMessage(raw, ws, fixtures, journal, defaults).catch((err: unknown) => {\n const msg = err instanceof Error ? err.message : \"Internal error\";\n logger.error(`WebSocket responses error: ${msg}`);\n try {\n ws.send(JSON.stringify(buildErrorEvent(msg, \"server_error\")));\n } catch (sendErr) {\n defaults.logger.debug(\n `Failed to send error to client: ${sendErr instanceof Error ? sendErr.message : \"unknown\"}`,\n );\n }\n }),\n );\n });\n}\n\nasync function processMessage(\n raw: string,\n ws: WebSocketConnection,\n fixtures: Fixture[],\n journal: Journal,\n defaults: {\n latency: number;\n chunkSize: number;\n replaySpeed?: number;\n model: string;\n logger: Logger;\n strict?: boolean;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n testId?: string;\n upgradeHeaders?: import(\"node:http\").IncomingHttpHeaders;\n },\n): Promise<void> {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n ws.send(\n JSON.stringify(\n buildErrorEvent(`Malformed JSON: ${detail}`, \"invalid_request_error\", \"invalid_json\"),\n ),\n );\n return;\n }\n\n if (!isResponseCreateMessage(parsed)) {\n ws.send(\n JSON.stringify(\n buildErrorEvent(\n 'Expected message type \"response.create\"',\n \"invalid_request_error\",\n \"invalid_message_type\",\n ),\n ),\n );\n return;\n }\n\n const responsesReq = {\n model: parsed.model ?? defaults.model,\n input: (parsed.input ?? []) as {\n role?: string;\n type?: string;\n content?: string | { type: string; text?: string }[];\n call_id?: string;\n name?: string;\n arguments?: string;\n output?: string;\n id?: string;\n }[],\n instructions: parsed.instructions,\n tools: parsed.tools as\n | {\n type: \"function\";\n name: string;\n description?: string;\n parameters?: object;\n strict?: boolean;\n }[]\n | undefined,\n tool_choice: parsed.tool_choice,\n stream: parsed.stream,\n temperature: parsed.temperature,\n max_output_tokens: parsed.max_output_tokens,\n include: (parsed as { include?: string[] }).include,\n store: (parsed as { store?: boolean }).store,\n };\n\n // Gate encrypted-reasoning emission identically to the HTTP transport so the\n // agent-framework#7233 stateless-replay path works over WebSocket too.\n const emitEncryptedReasoning = requestWantsEncryptedReasoning(responsesReq);\n // Synthesizing a reasoning item the fixture never declared takes the NARROWER\n // explicit-`include` gate, transport-identically to HTTP — a `store: false`\n // request with a summary-less fixture still gets no reasoning item.\n const synthesizeSummarylessReasoning = requestIncludesEncryptedReasoning(responsesReq);\n\n const completionReq = responsesToCompletionRequest(responsesReq);\n completionReq._endpointType = \"chat\";\n const contextHeader = defaults.upgradeHeaders?.[\"x-aimock-context\"];\n completionReq._context =\n typeof contextHeader === \"string\"\n ? contextHeader\n : Array.isArray(contextHeader) && contextHeader.length > 0\n ? contextHeader[0]\n : undefined;\n const testId = defaults.testId ?? DEFAULT_TEST_ID;\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n completionReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n }\n\n if (!fixture) {\n if (resolveStrictMode(defaults.strict, defaults.upgradeHeaders)) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n defaults.logger.error(strictNoMatchLogLine(\"WS\", \"/v1/responses\", skippedBySequenceOrTurn));\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, defaults.upgradeHeaders),\n },\n });\n ws.close(1008, strictMessage);\n return;\n }\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: {\n status: 404,\n fixture: null,\n ...strictOverrideField(defaults.strict, defaults.upgradeHeaders),\n },\n });\n ws.send(\n JSON.stringify(\n buildErrorEvent(\"No fixture matched\", \"invalid_request_error\", \"no_fixture_match\"),\n ),\n );\n return;\n }\n\n const response = await resolveResponse(fixture, completionReq);\n const latency = fixture.latency ?? defaults.latency;\n const chunkSize = Math.max(1, fixture.chunkSize ?? defaults.chunkSize);\n\n // The WS path has no per-request `req.headers`; strict is resolved from the\n // connection's upgrade headers (see the `!fixture` branch above). Used below\n // to gate the synthesized reasoning channel on the requested model's capability.\n const effectiveStrict = resolveStrictMode(defaults.strict, defaults.upgradeHeaders);\n\n // Error response\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status, fixture },\n });\n ws.send(\n JSON.stringify(\n buildErrorEvent(response.error.message, response.error.type, response.error.code),\n ),\n );\n return;\n }\n\n // Content + tool calls response (must be checked before isTextResponse / isToolCallResponse)\n if (isContentWithToolCallsResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n const events = buildContentWithToolCallsStreamEvents(\n response.content ?? \"\",\n response.toolCalls ?? [],\n completionReq.model,\n chunkSize,\n resolveReasoningForModel(\n response.reasoning,\n completionReq.model,\n effectiveStrict,\n defaults.logger,\n ),\n response.webSearches,\n extractOverrides(response),\n response.blocks,\n emitEncryptedReasoning,\n synthesizeSummarylessReasoning,\n );\n\n const interruption = createInterruptionSignal(fixture);\n const completed = await sendEvents(\n ws,\n events,\n latency,\n interruption?.signal,\n interruption?.tick,\n fixture.recordedTimings,\n fixture.replaySpeed ?? defaults.replaySpeed,\n );\n if (!completed) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n }\n interruption?.cleanup();\n return;\n }\n\n // Text response\n if (isTextResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n const events = buildTextStreamEvents(\n response.content,\n completionReq.model,\n chunkSize,\n resolveReasoningForModel(\n response.reasoning,\n completionReq.model,\n effectiveStrict,\n defaults.logger,\n ),\n response.webSearches,\n extractOverrides(response),\n emitEncryptedReasoning,\n synthesizeSummarylessReasoning,\n );\n const interruption = createInterruptionSignal(fixture);\n const completed = await sendEvents(\n ws,\n events,\n latency,\n interruption?.signal,\n interruption?.tick,\n fixture.recordedTimings,\n fixture.replaySpeed ?? defaults.replaySpeed,\n );\n if (!completed) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n }\n interruption?.cleanup();\n return;\n }\n\n // Tool call response\n if (isToolCallResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 200, fixture },\n });\n const events = buildToolCallStreamEvents(\n response.toolCalls,\n completionReq.model,\n chunkSize,\n // Gate the synthesized reasoning channel on the requested model's\n // capability, matching the WS text / content+tool branches and the HTTP\n // tool-only path so reasoning emission is transport-independent.\n resolveReasoningForModel(\n response.reasoning,\n completionReq.model,\n effectiveStrict,\n defaults.logger,\n ),\n response.webSearches,\n extractOverrides(response),\n emitEncryptedReasoning,\n synthesizeSummarylessReasoning,\n );\n const interruption = createInterruptionSignal(fixture);\n const completed = await sendEvents(\n ws,\n events,\n latency,\n interruption?.signal,\n interruption?.tick,\n fixture.recordedTimings,\n fixture.replaySpeed ?? defaults.replaySpeed,\n );\n if (!completed) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n }\n interruption?.cleanup();\n return;\n }\n\n // Unknown response type\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 500, fixture },\n });\n ws.send(\n JSON.stringify(\n buildErrorEvent(\"Fixture response did not match any known type\", \"server_error\"),\n ),\n );\n}\n\nasync function sendEvents(\n ws: WebSocketConnection,\n events: ResponsesSSEEvent[],\n latency: number,\n signal?: AbortSignal,\n onChunkSent?: () => void,\n recordedTimings?: import(\"./types.js\").RecordedTimings,\n replaySpeed?: number,\n): Promise<boolean> {\n let eventIndex = 0;\n for (const event of events) {\n if (ws.isClosed) return false;\n const chunkDelay = calculateDelay(eventIndex, undefined, latency, recordedTimings, replaySpeed);\n if (chunkDelay > 0) await delay(chunkDelay, signal);\n if (signal?.aborted) return false;\n if (ws.isClosed) return false;\n ws.send(JSON.stringify(event));\n eventIndex++;\n onChunkSent?.();\n if (signal?.aborted) return false;\n }\n return true;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ws-responses.d.cts","names":[],"sources":["../src/ws-responses.ts"],"sourcesContent":[],"mappings":";;;;;;;;AAwEW,iBAHK,wBAAA,CAGL,EAAA,EAFL,mBAEK,EAAA,QAAA,EADC,OACD,EAAA,EAAA,OAAA,EAAA,OAAA,EAAA,QAAA,EAAA;SAMC,EAAA,MAAA;WAEiB,EAAA,MAAA;aAA0B,CAAA,EAAA,MAAA;OAAqB,EAAA,MAAA;EAEhB,MAAA,EAJhD,MAIgD;;2BAF/B,0BAA0B;;mBAAqB,UAAA,CAEnC"}
1
+ {"version":3,"file":"ws-responses.d.cts","names":[],"sources":["../src/ws-responses.ts"],"sourcesContent":[],"mappings":";;;;;;;;AA0EW,iBAHK,wBAAA,CAGL,EAAA,EAFL,mBAEK,EAAA,QAAA,EADC,OACD,EAAA,EAAA,OAAA,EAAA,OAAA,EAAA,QAAA,EAAA;SAMC,EAAA,MAAA;WAEiB,EAAA,MAAA;aAA0B,CAAA,EAAA,MAAA;OAAqB,EAAA,MAAA;EAEhB,MAAA,EAJhD,MAIgD;;2BAF/B,0BAA0B;;mBAAqB,UAAA,CAEnC"}
@@ -1 +1 @@
1
- {"version":3,"file":"ws-responses.d.ts","names":[],"sources":["../src/ws-responses.ts"],"sourcesContent":[],"mappings":";;;;;;;;AAwEW,iBAHK,wBAAA,CAGL,EAAA,EAFL,mBAEK,EAAA,QAAA,EADC,OACD,EAAA,EAAA,OAAA,EAAA,OAAA,EAAA,QAAA,EAAA;SAMC,EAAA,MAAA;WAEiB,EAAA,MAAA;aAA0B,CAAA,EAAA,MAAA;OAAqB,EAAA,MAAA;EAEhB,MAAA,EAJhD,MAIgD;;2BAF/B,0BAA0B;;mBAAqB,UAAA,CAEnC"}
1
+ {"version":3,"file":"ws-responses.d.ts","names":[],"sources":["../src/ws-responses.ts"],"sourcesContent":[],"mappings":";;;;;;;;AA0EW,iBAHK,wBAAA,CAGL,EAAA,EAFL,mBAEK,EAAA,QAAA,EADC,OACD,EAAA,EAAA,OAAA,EAAA,OAAA,EAAA,QAAA,EAAA;SAMC,EAAA,MAAA;WAEiB,EAAA,MAAA;aAA0B,CAAA,EAAA,MAAA;OAAqB,EAAA,MAAA;EAEhB,MAAA,EAJhD,MAIgD;;2BAF/B,0BAA0B;;mBAAqB,UAAA,CAEnC"}
@@ -4,7 +4,7 @@ import "./journal.js";
4
4
  import { matchFixtureDiagnostic } from "./router.js";
5
5
  import { calculateDelay, delay } from "./sse-writer.js";
6
6
  import { createInterruptionSignal } from "./interruption.js";
7
- import { buildContentWithToolCallsStreamEvents, buildTextStreamEvents, buildToolCallStreamEvents, responsesToCompletionRequest } from "./responses.js";
7
+ import { buildContentWithToolCallsStreamEvents, buildTextStreamEvents, buildToolCallStreamEvents, requestIncludesEncryptedReasoning, requestWantsEncryptedReasoning, responsesToCompletionRequest } from "./responses.js";
8
8
 
9
9
  //#region src/ws-responses.ts
10
10
  function isResponseCreateMessage(msg) {
@@ -48,7 +48,7 @@ async function processMessage(raw, ws, fixtures, journal, defaults) {
48
48
  ws.send(JSON.stringify(buildErrorEvent("Expected message type \"response.create\"", "invalid_request_error", "invalid_message_type")));
49
49
  return;
50
50
  }
51
- const completionReq = responsesToCompletionRequest({
51
+ const responsesReq = {
52
52
  model: parsed.model ?? defaults.model,
53
53
  input: parsed.input ?? [],
54
54
  instructions: parsed.instructions,
@@ -56,8 +56,13 @@ async function processMessage(raw, ws, fixtures, journal, defaults) {
56
56
  tool_choice: parsed.tool_choice,
57
57
  stream: parsed.stream,
58
58
  temperature: parsed.temperature,
59
- max_output_tokens: parsed.max_output_tokens
60
- });
59
+ max_output_tokens: parsed.max_output_tokens,
60
+ include: parsed.include,
61
+ store: parsed.store
62
+ };
63
+ const emitEncryptedReasoning = requestWantsEncryptedReasoning(responsesReq);
64
+ const synthesizeSummarylessReasoning = requestIncludesEncryptedReasoning(responsesReq);
65
+ const completionReq = responsesToCompletionRequest(responsesReq);
61
66
  completionReq._endpointType = "chat";
62
67
  const contextHeader = defaults.upgradeHeaders?.["x-aimock-context"];
63
68
  completionReq._context = typeof contextHeader === "string" ? contextHeader : Array.isArray(contextHeader) && contextHeader.length > 0 ? contextHeader[0] : void 0;
@@ -126,7 +131,7 @@ async function processMessage(raw, ws, fixtures, journal, defaults) {
126
131
  fixture
127
132
  }
128
133
  });
129
- const events = buildContentWithToolCallsStreamEvents(response.content ?? "", response.toolCalls ?? [], completionReq.model, chunkSize, resolveReasoningForModel(response.reasoning, completionReq.model, effectiveStrict, defaults.logger), response.webSearches, extractOverrides(response), response.blocks);
134
+ const events = buildContentWithToolCallsStreamEvents(response.content ?? "", response.toolCalls ?? [], completionReq.model, chunkSize, resolveReasoningForModel(response.reasoning, completionReq.model, effectiveStrict, defaults.logger), response.webSearches, extractOverrides(response), response.blocks, emitEncryptedReasoning, synthesizeSummarylessReasoning);
130
135
  const interruption = createInterruptionSignal(fixture);
131
136
  if (!await sendEvents(ws, events, latency, interruption?.signal, interruption?.tick, fixture.recordedTimings, fixture.replaySpeed ?? defaults.replaySpeed)) {
132
137
  ws.destroy();
@@ -147,7 +152,7 @@ async function processMessage(raw, ws, fixtures, journal, defaults) {
147
152
  fixture
148
153
  }
149
154
  });
150
- const events = buildTextStreamEvents(response.content, completionReq.model, chunkSize, resolveReasoningForModel(response.reasoning, completionReq.model, effectiveStrict, defaults.logger), response.webSearches, extractOverrides(response));
155
+ const events = buildTextStreamEvents(response.content, completionReq.model, chunkSize, resolveReasoningForModel(response.reasoning, completionReq.model, effectiveStrict, defaults.logger), response.webSearches, extractOverrides(response), emitEncryptedReasoning, synthesizeSummarylessReasoning);
151
156
  const interruption = createInterruptionSignal(fixture);
152
157
  if (!await sendEvents(ws, events, latency, interruption?.signal, interruption?.tick, fixture.recordedTimings, fixture.replaySpeed ?? defaults.replaySpeed)) {
153
158
  ws.destroy();
@@ -168,7 +173,7 @@ async function processMessage(raw, ws, fixtures, journal, defaults) {
168
173
  fixture
169
174
  }
170
175
  });
171
- const events = buildToolCallStreamEvents(response.toolCalls, completionReq.model, chunkSize, resolveReasoningForModel(response.reasoning, completionReq.model, effectiveStrict, defaults.logger), response.webSearches, extractOverrides(response));
176
+ const events = buildToolCallStreamEvents(response.toolCalls, completionReq.model, chunkSize, resolveReasoningForModel(response.reasoning, completionReq.model, effectiveStrict, defaults.logger), response.webSearches, extractOverrides(response), emitEncryptedReasoning, synthesizeSummarylessReasoning);
172
177
  const interruption = createInterruptionSignal(fixture);
173
178
  if (!await sendEvents(ws, events, latency, interruption?.signal, interruption?.tick, fixture.recordedTimings, fixture.replaySpeed ?? defaults.replaySpeed)) {
174
179
  ws.destroy();
@@ -1 +1 @@
1
- {"version":3,"file":"ws-responses.js","names":[],"sources":["../src/ws-responses.ts"],"sourcesContent":["/**\n * WebSocket handler for OpenAI Responses API.\n *\n * Accepts `{ type: \"response.create\", model: \"...\", input: [...] }` messages over\n * WebSocket and sends back the same Responses API SSE events as the HTTP\n * handler, but as individual WebSocket text frames.\n */\n\nimport type { ChatCompletionRequest, Fixture } from \"./types.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport {\n responsesToCompletionRequest,\n buildTextStreamEvents,\n buildToolCallStreamEvents,\n buildContentWithToolCallsStreamEvents,\n type ResponsesSSEEvent,\n} from \"./responses.js\";\nimport {\n isTextResponse,\n isToolCallResponse,\n isContentWithToolCallsResponse,\n isErrorResponse,\n extractOverrides,\n resolveResponse,\n resolveStrictMode,\n resolveReasoningForModel,\n strictOverrideField,\n flattenHeaders,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { createInterruptionSignal } from \"./interruption.js\";\nimport { delay, calculateDelay } from \"./sse-writer.js\";\nimport { DEFAULT_TEST_ID, type Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\nimport type { WebSocketConnection } from \"./ws-framing.js\";\n\ninterface ResponseCreateMessage {\n type: \"response.create\";\n model?: string;\n input?: unknown[];\n instructions?: string;\n tools?: unknown[];\n tool_choice?: string | object;\n stream?: boolean;\n temperature?: number;\n max_output_tokens?: number;\n [key: string]: unknown;\n}\n\nfunction isResponseCreateMessage(msg: unknown): msg is ResponseCreateMessage {\n return (\n typeof msg === \"object\" &&\n msg !== null &&\n (msg as ResponseCreateMessage).type === \"response.create\"\n );\n}\n\nfunction buildErrorEvent(\n message: string,\n type = \"invalid_request_error\",\n code?: string,\n): ResponsesSSEEvent {\n return {\n type: \"error\",\n error: { message, type, code },\n };\n}\n\nexport function handleWebSocketResponses(\n ws: WebSocketConnection,\n fixtures: Fixture[],\n journal: Journal,\n defaults: {\n latency: number;\n chunkSize: number;\n replaySpeed?: number;\n model: string;\n logger: Logger;\n strict?: boolean;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n testId?: string;\n upgradeHeaders?: import(\"node:http\").IncomingHttpHeaders;\n },\n): void {\n const { logger } = defaults;\n // Serialize message processing to prevent event interleaving\n let pending = Promise.resolve();\n ws.on(\"message\", (raw: string) => {\n pending = pending.then(() =>\n processMessage(raw, ws, fixtures, journal, defaults).catch((err: unknown) => {\n const msg = err instanceof Error ? err.message : \"Internal error\";\n logger.error(`WebSocket responses error: ${msg}`);\n try {\n ws.send(JSON.stringify(buildErrorEvent(msg, \"server_error\")));\n } catch (sendErr) {\n defaults.logger.debug(\n `Failed to send error to client: ${sendErr instanceof Error ? sendErr.message : \"unknown\"}`,\n );\n }\n }),\n );\n });\n}\n\nasync function processMessage(\n raw: string,\n ws: WebSocketConnection,\n fixtures: Fixture[],\n journal: Journal,\n defaults: {\n latency: number;\n chunkSize: number;\n replaySpeed?: number;\n model: string;\n logger: Logger;\n strict?: boolean;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n testId?: string;\n upgradeHeaders?: import(\"node:http\").IncomingHttpHeaders;\n },\n): Promise<void> {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n ws.send(\n JSON.stringify(\n buildErrorEvent(`Malformed JSON: ${detail}`, \"invalid_request_error\", \"invalid_json\"),\n ),\n );\n return;\n }\n\n if (!isResponseCreateMessage(parsed)) {\n ws.send(\n JSON.stringify(\n buildErrorEvent(\n 'Expected message type \"response.create\"',\n \"invalid_request_error\",\n \"invalid_message_type\",\n ),\n ),\n );\n return;\n }\n\n const responsesReq = {\n model: parsed.model ?? defaults.model,\n input: (parsed.input ?? []) as {\n role?: string;\n type?: string;\n content?: string | { type: string; text?: string }[];\n call_id?: string;\n name?: string;\n arguments?: string;\n output?: string;\n id?: string;\n }[],\n instructions: parsed.instructions,\n tools: parsed.tools as\n | {\n type: \"function\";\n name: string;\n description?: string;\n parameters?: object;\n strict?: boolean;\n }[]\n | undefined,\n tool_choice: parsed.tool_choice,\n stream: parsed.stream,\n temperature: parsed.temperature,\n max_output_tokens: parsed.max_output_tokens,\n };\n\n const completionReq = responsesToCompletionRequest(responsesReq);\n completionReq._endpointType = \"chat\";\n const contextHeader = defaults.upgradeHeaders?.[\"x-aimock-context\"];\n completionReq._context =\n typeof contextHeader === \"string\"\n ? contextHeader\n : Array.isArray(contextHeader) && contextHeader.length > 0\n ? contextHeader[0]\n : undefined;\n const testId = defaults.testId ?? DEFAULT_TEST_ID;\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n completionReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n }\n\n if (!fixture) {\n if (resolveStrictMode(defaults.strict, defaults.upgradeHeaders)) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n defaults.logger.error(strictNoMatchLogLine(\"WS\", \"/v1/responses\", skippedBySequenceOrTurn));\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, defaults.upgradeHeaders),\n },\n });\n ws.close(1008, strictMessage);\n return;\n }\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: {\n status: 404,\n fixture: null,\n ...strictOverrideField(defaults.strict, defaults.upgradeHeaders),\n },\n });\n ws.send(\n JSON.stringify(\n buildErrorEvent(\"No fixture matched\", \"invalid_request_error\", \"no_fixture_match\"),\n ),\n );\n return;\n }\n\n const response = await resolveResponse(fixture, completionReq);\n const latency = fixture.latency ?? defaults.latency;\n const chunkSize = Math.max(1, fixture.chunkSize ?? defaults.chunkSize);\n\n // The WS path has no per-request `req.headers`; strict is resolved from the\n // connection's upgrade headers (see the `!fixture` branch above). Used below\n // to gate the synthesized reasoning channel on the requested model's capability.\n const effectiveStrict = resolveStrictMode(defaults.strict, defaults.upgradeHeaders);\n\n // Error response\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status, fixture },\n });\n ws.send(\n JSON.stringify(\n buildErrorEvent(response.error.message, response.error.type, response.error.code),\n ),\n );\n return;\n }\n\n // Content + tool calls response (must be checked before isTextResponse / isToolCallResponse)\n if (isContentWithToolCallsResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n const events = buildContentWithToolCallsStreamEvents(\n response.content ?? \"\",\n response.toolCalls ?? [],\n completionReq.model,\n chunkSize,\n resolveReasoningForModel(\n response.reasoning,\n completionReq.model,\n effectiveStrict,\n defaults.logger,\n ),\n response.webSearches,\n extractOverrides(response),\n response.blocks,\n );\n\n const interruption = createInterruptionSignal(fixture);\n const completed = await sendEvents(\n ws,\n events,\n latency,\n interruption?.signal,\n interruption?.tick,\n fixture.recordedTimings,\n fixture.replaySpeed ?? defaults.replaySpeed,\n );\n if (!completed) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n }\n interruption?.cleanup();\n return;\n }\n\n // Text response\n if (isTextResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n const events = buildTextStreamEvents(\n response.content,\n completionReq.model,\n chunkSize,\n resolveReasoningForModel(\n response.reasoning,\n completionReq.model,\n effectiveStrict,\n defaults.logger,\n ),\n response.webSearches,\n extractOverrides(response),\n );\n const interruption = createInterruptionSignal(fixture);\n const completed = await sendEvents(\n ws,\n events,\n latency,\n interruption?.signal,\n interruption?.tick,\n fixture.recordedTimings,\n fixture.replaySpeed ?? defaults.replaySpeed,\n );\n if (!completed) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n }\n interruption?.cleanup();\n return;\n }\n\n // Tool call response\n if (isToolCallResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 200, fixture },\n });\n const events = buildToolCallStreamEvents(\n response.toolCalls,\n completionReq.model,\n chunkSize,\n // Gate the synthesized reasoning channel on the requested model's\n // capability, matching the WS text / content+tool branches and the HTTP\n // tool-only path so reasoning emission is transport-independent.\n resolveReasoningForModel(\n response.reasoning,\n completionReq.model,\n effectiveStrict,\n defaults.logger,\n ),\n response.webSearches,\n extractOverrides(response),\n );\n const interruption = createInterruptionSignal(fixture);\n const completed = await sendEvents(\n ws,\n events,\n latency,\n interruption?.signal,\n interruption?.tick,\n fixture.recordedTimings,\n fixture.replaySpeed ?? defaults.replaySpeed,\n );\n if (!completed) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n }\n interruption?.cleanup();\n return;\n }\n\n // Unknown response type\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 500, fixture },\n });\n ws.send(\n JSON.stringify(\n buildErrorEvent(\"Fixture response did not match any known type\", \"server_error\"),\n ),\n );\n}\n\nasync function sendEvents(\n ws: WebSocketConnection,\n events: ResponsesSSEEvent[],\n latency: number,\n signal?: AbortSignal,\n onChunkSent?: () => void,\n recordedTimings?: import(\"./types.js\").RecordedTimings,\n replaySpeed?: number,\n): Promise<boolean> {\n let eventIndex = 0;\n for (const event of events) {\n if (ws.isClosed) return false;\n const chunkDelay = calculateDelay(eventIndex, undefined, latency, recordedTimings, replaySpeed);\n if (chunkDelay > 0) await delay(chunkDelay, signal);\n if (signal?.aborted) return false;\n if (ws.isClosed) return false;\n ws.send(JSON.stringify(event));\n eventIndex++;\n onChunkSent?.();\n if (signal?.aborted) return false;\n }\n return true;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ws-responses.js","names":[],"sources":["../src/ws-responses.ts"],"sourcesContent":["/**\n * WebSocket handler for OpenAI Responses API.\n *\n * Accepts `{ type: \"response.create\", model: \"...\", input: [...] }` messages over\n * WebSocket and sends back the same Responses API SSE events as the HTTP\n * handler, but as individual WebSocket text frames.\n */\n\nimport type { ChatCompletionRequest, Fixture } from \"./types.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport {\n responsesToCompletionRequest,\n requestIncludesEncryptedReasoning,\n requestWantsEncryptedReasoning,\n buildTextStreamEvents,\n buildToolCallStreamEvents,\n buildContentWithToolCallsStreamEvents,\n type ResponsesSSEEvent,\n} from \"./responses.js\";\nimport {\n isTextResponse,\n isToolCallResponse,\n isContentWithToolCallsResponse,\n isErrorResponse,\n extractOverrides,\n resolveResponse,\n resolveStrictMode,\n resolveReasoningForModel,\n strictOverrideField,\n flattenHeaders,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { createInterruptionSignal } from \"./interruption.js\";\nimport { delay, calculateDelay } from \"./sse-writer.js\";\nimport { DEFAULT_TEST_ID, type Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\nimport type { WebSocketConnection } from \"./ws-framing.js\";\n\ninterface ResponseCreateMessage {\n type: \"response.create\";\n model?: string;\n input?: unknown[];\n instructions?: string;\n tools?: unknown[];\n tool_choice?: string | object;\n stream?: boolean;\n temperature?: number;\n max_output_tokens?: number;\n [key: string]: unknown;\n}\n\nfunction isResponseCreateMessage(msg: unknown): msg is ResponseCreateMessage {\n return (\n typeof msg === \"object\" &&\n msg !== null &&\n (msg as ResponseCreateMessage).type === \"response.create\"\n );\n}\n\nfunction buildErrorEvent(\n message: string,\n type = \"invalid_request_error\",\n code?: string,\n): ResponsesSSEEvent {\n return {\n type: \"error\",\n error: { message, type, code },\n };\n}\n\nexport function handleWebSocketResponses(\n ws: WebSocketConnection,\n fixtures: Fixture[],\n journal: Journal,\n defaults: {\n latency: number;\n chunkSize: number;\n replaySpeed?: number;\n model: string;\n logger: Logger;\n strict?: boolean;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n testId?: string;\n upgradeHeaders?: import(\"node:http\").IncomingHttpHeaders;\n },\n): void {\n const { logger } = defaults;\n // Serialize message processing to prevent event interleaving\n let pending = Promise.resolve();\n ws.on(\"message\", (raw: string) => {\n pending = pending.then(() =>\n processMessage(raw, ws, fixtures, journal, defaults).catch((err: unknown) => {\n const msg = err instanceof Error ? err.message : \"Internal error\";\n logger.error(`WebSocket responses error: ${msg}`);\n try {\n ws.send(JSON.stringify(buildErrorEvent(msg, \"server_error\")));\n } catch (sendErr) {\n defaults.logger.debug(\n `Failed to send error to client: ${sendErr instanceof Error ? sendErr.message : \"unknown\"}`,\n );\n }\n }),\n );\n });\n}\n\nasync function processMessage(\n raw: string,\n ws: WebSocketConnection,\n fixtures: Fixture[],\n journal: Journal,\n defaults: {\n latency: number;\n chunkSize: number;\n replaySpeed?: number;\n model: string;\n logger: Logger;\n strict?: boolean;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n testId?: string;\n upgradeHeaders?: import(\"node:http\").IncomingHttpHeaders;\n },\n): Promise<void> {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n ws.send(\n JSON.stringify(\n buildErrorEvent(`Malformed JSON: ${detail}`, \"invalid_request_error\", \"invalid_json\"),\n ),\n );\n return;\n }\n\n if (!isResponseCreateMessage(parsed)) {\n ws.send(\n JSON.stringify(\n buildErrorEvent(\n 'Expected message type \"response.create\"',\n \"invalid_request_error\",\n \"invalid_message_type\",\n ),\n ),\n );\n return;\n }\n\n const responsesReq = {\n model: parsed.model ?? defaults.model,\n input: (parsed.input ?? []) as {\n role?: string;\n type?: string;\n content?: string | { type: string; text?: string }[];\n call_id?: string;\n name?: string;\n arguments?: string;\n output?: string;\n id?: string;\n }[],\n instructions: parsed.instructions,\n tools: parsed.tools as\n | {\n type: \"function\";\n name: string;\n description?: string;\n parameters?: object;\n strict?: boolean;\n }[]\n | undefined,\n tool_choice: parsed.tool_choice,\n stream: parsed.stream,\n temperature: parsed.temperature,\n max_output_tokens: parsed.max_output_tokens,\n include: (parsed as { include?: string[] }).include,\n store: (parsed as { store?: boolean }).store,\n };\n\n // Gate encrypted-reasoning emission identically to the HTTP transport so the\n // agent-framework#7233 stateless-replay path works over WebSocket too.\n const emitEncryptedReasoning = requestWantsEncryptedReasoning(responsesReq);\n // Synthesizing a reasoning item the fixture never declared takes the NARROWER\n // explicit-`include` gate, transport-identically to HTTP — a `store: false`\n // request with a summary-less fixture still gets no reasoning item.\n const synthesizeSummarylessReasoning = requestIncludesEncryptedReasoning(responsesReq);\n\n const completionReq = responsesToCompletionRequest(responsesReq);\n completionReq._endpointType = \"chat\";\n const contextHeader = defaults.upgradeHeaders?.[\"x-aimock-context\"];\n completionReq._context =\n typeof contextHeader === \"string\"\n ? contextHeader\n : Array.isArray(contextHeader) && contextHeader.length > 0\n ? contextHeader[0]\n : undefined;\n const testId = defaults.testId ?? DEFAULT_TEST_ID;\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n completionReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n }\n\n if (!fixture) {\n if (resolveStrictMode(defaults.strict, defaults.upgradeHeaders)) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n defaults.logger.error(strictNoMatchLogLine(\"WS\", \"/v1/responses\", skippedBySequenceOrTurn));\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, defaults.upgradeHeaders),\n },\n });\n ws.close(1008, strictMessage);\n return;\n }\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: {\n status: 404,\n fixture: null,\n ...strictOverrideField(defaults.strict, defaults.upgradeHeaders),\n },\n });\n ws.send(\n JSON.stringify(\n buildErrorEvent(\"No fixture matched\", \"invalid_request_error\", \"no_fixture_match\"),\n ),\n );\n return;\n }\n\n const response = await resolveResponse(fixture, completionReq);\n const latency = fixture.latency ?? defaults.latency;\n const chunkSize = Math.max(1, fixture.chunkSize ?? defaults.chunkSize);\n\n // The WS path has no per-request `req.headers`; strict is resolved from the\n // connection's upgrade headers (see the `!fixture` branch above). Used below\n // to gate the synthesized reasoning channel on the requested model's capability.\n const effectiveStrict = resolveStrictMode(defaults.strict, defaults.upgradeHeaders);\n\n // Error response\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status, fixture },\n });\n ws.send(\n JSON.stringify(\n buildErrorEvent(response.error.message, response.error.type, response.error.code),\n ),\n );\n return;\n }\n\n // Content + tool calls response (must be checked before isTextResponse / isToolCallResponse)\n if (isContentWithToolCallsResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n const events = buildContentWithToolCallsStreamEvents(\n response.content ?? \"\",\n response.toolCalls ?? [],\n completionReq.model,\n chunkSize,\n resolveReasoningForModel(\n response.reasoning,\n completionReq.model,\n effectiveStrict,\n defaults.logger,\n ),\n response.webSearches,\n extractOverrides(response),\n response.blocks,\n emitEncryptedReasoning,\n synthesizeSummarylessReasoning,\n );\n\n const interruption = createInterruptionSignal(fixture);\n const completed = await sendEvents(\n ws,\n events,\n latency,\n interruption?.signal,\n interruption?.tick,\n fixture.recordedTimings,\n fixture.replaySpeed ?? defaults.replaySpeed,\n );\n if (!completed) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n }\n interruption?.cleanup();\n return;\n }\n\n // Text response\n if (isTextResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n const events = buildTextStreamEvents(\n response.content,\n completionReq.model,\n chunkSize,\n resolveReasoningForModel(\n response.reasoning,\n completionReq.model,\n effectiveStrict,\n defaults.logger,\n ),\n response.webSearches,\n extractOverrides(response),\n emitEncryptedReasoning,\n synthesizeSummarylessReasoning,\n );\n const interruption = createInterruptionSignal(fixture);\n const completed = await sendEvents(\n ws,\n events,\n latency,\n interruption?.signal,\n interruption?.tick,\n fixture.recordedTimings,\n fixture.replaySpeed ?? defaults.replaySpeed,\n );\n if (!completed) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n }\n interruption?.cleanup();\n return;\n }\n\n // Tool call response\n if (isToolCallResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 200, fixture },\n });\n const events = buildToolCallStreamEvents(\n response.toolCalls,\n completionReq.model,\n chunkSize,\n // Gate the synthesized reasoning channel on the requested model's\n // capability, matching the WS text / content+tool branches and the HTTP\n // tool-only path so reasoning emission is transport-independent.\n resolveReasoningForModel(\n response.reasoning,\n completionReq.model,\n effectiveStrict,\n defaults.logger,\n ),\n response.webSearches,\n extractOverrides(response),\n emitEncryptedReasoning,\n synthesizeSummarylessReasoning,\n );\n const interruption = createInterruptionSignal(fixture);\n const completed = await sendEvents(\n ws,\n events,\n latency,\n interruption?.signal,\n interruption?.tick,\n fixture.recordedTimings,\n fixture.replaySpeed ?? defaults.replaySpeed,\n );\n if (!completed) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n }\n interruption?.cleanup();\n return;\n }\n\n // Unknown response type\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 500, fixture },\n });\n ws.send(\n JSON.stringify(\n buildErrorEvent(\"Fixture response did not match any known type\", \"server_error\"),\n ),\n );\n}\n\nasync function sendEvents(\n ws: WebSocketConnection,\n events: ResponsesSSEEvent[],\n latency: number,\n signal?: AbortSignal,\n onChunkSent?: () => void,\n recordedTimings?: import(\"./types.js\").RecordedTimings,\n replaySpeed?: number,\n): Promise<boolean> {\n let eventIndex = 0;\n for (const event of events) {\n if (ws.isClosed) return false;\n const chunkDelay = calculateDelay(eventIndex, undefined, latency, recordedTimings, replaySpeed);\n if (chunkDelay > 0) await delay(chunkDelay, signal);\n if (signal?.aborted) return false;\n if (ws.isClosed) return false;\n ws.send(JSON.stringify(event));\n eventIndex++;\n onChunkSent?.();\n if (signal?.aborted) return false;\n }\n return true;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@copilotkit/aimock",
3
- "version": "1.37.4",
4
- "description": "Mock infrastructure for AI application testing — LLM APIs, image generation, image editing, text-to-speech, transcription, audio translation, audio generation, video generation, embeddings, MCP tools, A2A agents, AG-UI event streams, vector databases, search, rerank, and moderation. One package, one port, zero dependencies.",
3
+ "version": "1.39.0",
4
+ "description": "<div align=\"center\">",
5
5
  "license": "MIT",
6
6
  "keywords": [
7
7
  "mock",
@@ -202,6 +202,7 @@
202
202
  "husky": "^9.1.7",
203
203
  "lint-staged": "^16.3.2",
204
204
  "openai": "^4.0.0",
205
+ "pkg-pr-new": "0.0.86",
205
206
  "prettier": "^3.6.2",
206
207
  "publint": "^0.3.12",
207
208
  "tsdown": "^0.12.5",
@@ -7,7 +7,7 @@ description: Use when writing test fixtures for @copilotkit/aimock — mock LLM
7
7
 
8
8
  ## What aimock Is
9
9
 
10
- aimock is a zero-dependency mock infrastructure for AI apps. Fixture-driven. Multi-provider (OpenAI, Anthropic, Gemini, Gemini Interactions, AWS Bedrock, Azure OpenAI, Vertex AI, Ollama, Cohere). Multimedia endpoints (image generation, text-to-speech, audio transcription, video generation). MCP, A2A, AG-UI, and vector DB mocking. Runs a real HTTP server on a real port — works across processes, unlike MSW-style interceptors. WebSocket support for OpenAI Responses/Realtime and Gemini Live APIs. Record-and-replay for all endpoints including multimedia. Chaos testing and Prometheus metrics.
10
+ aimock is a zero-dependency mock infrastructure for AI apps. Fixture-driven. Multi-provider (OpenAI, Anthropic, Gemini, Gemini Interactions, AWS Bedrock, Azure OpenAI, Vertex AI, Ollama, Cohere, OpenRouter). Multimedia endpoints (image generation, text-to-speech, audio transcription, video generation). MCP, A2A, AG-UI, and vector DB mocking. Runs a real HTTP server on a real port — works across processes, unlike MSW-style interceptors. WebSocket support for OpenAI Responses/Realtime and Gemini Live APIs. Record-and-replay for all endpoints including multimedia. Chaos testing and Prometheus metrics.
11
11
 
12
12
  ## Core Mental Model
13
13
 
@@ -420,6 +420,8 @@ All providers share the same fixture pool — write fixtures once, they work for
420
420
  | `POST /api/generate` | Ollama | HTTP |
421
421
  | `GET /api/tags` | Ollama | HTTP |
422
422
  | `POST /v2/chat` | Cohere | HTTP |
423
+ | `POST /api/v1/chat/completions` | OpenRouter | HTTP |
424
+ | `GET /api/v1/models` · `/api/v1/key` · `/api/v1/credits` | OpenRouter | HTTP |
423
425
  | `GET /metrics` | — | HTTP |
424
426
  | `GET /v1/models` | OpenAI-compat | HTTP |
425
427
  | `WS /v1/responses` | OpenAI | WebSocket |
@@ -436,15 +438,19 @@ All providers share the same fixture pool — write fixtures once, they work for
436
438
 
437
439
  Fixture responses can include optional override fields to control auto-generated envelope values. These are merged into the provider-specific response format (OpenAI, Claude, Gemini, Responses API).
438
440
 
439
- | Field | Type | Default | Description |
440
- | ------------------- | ------ | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
441
- | `id` | string | auto-generated | Override response ID (e.g., `chatcmpl-custom`) |
442
- | `created` | number | `Date.now()/1000` | Override Unix timestamp |
443
- | `model` | string | echoes request | Override model name in response |
444
- | `usage` | object | zeroed | Override token counts: `{ prompt_tokens, completion_tokens, total_tokens }`. OpenAI Chat includes usage in response body; Responses API uses `response.usage`. When omitted, auto-computed from content length |
445
- | `finishReason` | string | `"stop"` / `"tool_calls"` | Override finish reason. Mappings: `stop` -> `end_turn` (Claude), `STOP` (Gemini); `tool_calls` -> `tool_use` (Claude), `FUNCTION_CALL` (Gemini); `length` -> `max_tokens` (Claude), `MAX_TOKENS` (Gemini); `content_filter` -> `SAFETY` (Gemini), `failed` (Responses API) |
446
- | `role` | string | `"assistant"` | Override message role |
447
- | `systemFingerprint` | string | (omitted) | Add `system_fingerprint` to response |
441
+ | Field | Type | Default | Description |
442
+ | -------------------- | ------ | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
443
+ | `id` | string | auto-generated | Override response ID (e.g., `chatcmpl-custom`) |
444
+ | `created` | number | `Date.now()/1000` | Override Unix timestamp |
445
+ | `model` | string | echoes request | Override model name in response |
446
+ | `usage` | object | zeroed | Override token counts: `{ prompt_tokens, completion_tokens, total_tokens }`. OpenAI Chat includes usage in response body; Responses API uses `response.usage`. When omitted, auto-computed from content length |
447
+ | `finishReason` | string | `"stop"` / `"tool_calls"` | Override finish reason. Mappings: `stop` -> `end_turn` (Claude), `STOP` (Gemini); `tool_calls` -> `tool_use` (Claude), `FUNCTION_CALL` (Gemini); `length` -> `max_tokens` (Claude), `MAX_TOKENS` (Gemini); `content_filter` -> `SAFETY` (Gemini), `failed` (Responses API) |
448
+ | `role` | string | `"assistant"` | Override message role |
449
+ | `systemFingerprint` | string | (omitted) | Add `system_fingerprint` to response |
450
+ | `provider` | string | slug author | OpenRouter only: top-level serving-provider display name (default = the winning model slug's author). Override to assert who served the request |
451
+ | `nativeFinishReason` | string | mirrors `finishReason` | OpenRouter only: the raw upstream `native_finish_reason` alongside the normalized `finish_reason` |
452
+ | `usage.cost` | number | (omitted) | OpenRouter only: per-request `usage.cost` (scriptable — powers budget-guard tests). When set, `usage.cost_details` is emitted too. Never fabricated when omitted |
453
+ | `usage.is_byok` | bool | (omitted) | OpenRouter only: emit `usage.is_byok`. Also `usage.prompt_tokens_details`, `usage.completion_tokens_details` — emitted only when set |
448
454
 
449
455
  ### Example
450
456
 
@@ -473,17 +479,45 @@ mock.onMessage("hello", {
473
479
 
474
480
  These fields map correctly across all provider formats — for example, `finishReason: "stop"` becomes `finish_reason: "stop"` in OpenAI, `stop_reason: "end_turn"` in Claude, and `finishReason: "STOP"` in Gemini.
475
481
 
482
+ ## OpenRouter (chat / router)
483
+
484
+ A request whose path starts with `/api/v1/` (point the OpenAI SDK at a `baseURL` ending `/api/v1`) is shaped as OpenRouter: `gen-` id, top-level `provider`, per-choice `native_finish_reason`, always-present `system_fingerprint`/`service_tier` (null by default), an always-present `message.reasoning` (null unless a fixture supplies reasoning and the model is reasoning-capable), and a rich `usage`. Requests on the plain `/v1/...` base are untouched OpenAI. Same fixture pool — the fields above are the only additions.
485
+
486
+ - **Scriptable cost / provider / finish reason**: set `provider`, `nativeFinishReason`, and `usage.cost` on the response (see the overrides table). `cost`/`cost_details` are emitted only when a fixture supplies `cost` — aimock never fabricates a cost.
487
+ - **`models[]` fallback (router failover)**: when the request body carries `models: [m1, m2, ...]`, aimock walks `[model, ...models]` in order and serves the first fixture that returns a NON-error response. A `429`/`503` error fixture on a candidate simulates a RUNTIME provider failure and falls through to the next candidate; the winning slug is echoed back as the top-level `model` (assert failover via `response.model`). Model the primary's "failure" as a 429/503 — an unknown/invalid model is just a fixture miss (aimock does not replicate OpenRouter's up-front invalid-model 400).
488
+ - **Terminal (non-failover) error class — `fallthrough: false`**: real OpenRouter fails over inconsistently by error class (a `403 budget-exceeded` / generic "provider returned error" is served as terminal and does NOT advance to the next candidate, while 429/503 usually do). Set `fallthrough: false` on an error fixture to make it terminal: the fallback loop stops and serves that error even when a good candidate follows. Absent / `true` keeps the default fall-through. Composes with `provider.allow_fallbacks`: fall-through happens only when both allow it (if either says stop, the error is terminal). Use it to reproduce the exact provider error a dev's app must handle itself. `{ error: { message: "budget exceeded" }, status: 403, fallthrough: false }`
489
+ - **Keepalive**: set the fixture option `openRouterProcessing: true` to emit one `: OPENROUTER PROCESSING` SSE comment before the first data frame (opt-in, default off).
490
+ - **Error envelope**: OpenRouter errors are `{ error: { message, code } }` (numeric `code` == HTTP status), with optional free-form `metadata`.
491
+
492
+ ```typescript
493
+ // primary is a runtime 429, fallback answers
494
+ mock.on(
495
+ { model: "openai/gpt-4o", userMessage: "route" },
496
+ { error: { message: "rate limited" }, status: 429 },
497
+ );
498
+ mock.on(
499
+ { model: "anthropic/claude-3.5-sonnet", userMessage: "route" },
500
+ {
501
+ content: "served by the fallback",
502
+ provider: "Anthropic",
503
+ usage: { cost: 0.0021 },
504
+ },
505
+ );
506
+ // POST /api/v1/chat/completions { model: "openai/gpt-4o", models: [...], ... }
507
+ // → response.model === "anthropic/claude-3.5-sonnet"
508
+ ```
509
+
476
510
  ## Provider Support Matrix
477
511
 
478
- | Feature | OpenAI Chat | OpenAI Responses | Claude | Gemini | Gemini Int. | Bedrock | Azure | Ollama | Cohere |
479
- | -------------------- | ----------- | ---------------- | ------ | ------ | ----------- | ------- | ----- | ------ | ------ |
480
- | Text | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
481
- | Tool Calls | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
482
- | Content + Tool Calls | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
483
- | Streaming | SSE | SSE | SSE | SSE | SSE | Binary | SSE | NDJSON | SSE |
484
- | Reasoning | Yes | Yes | Yes | Yes | -- | Yes | Yes | -- | -- |
485
- | Web Searches | -- | Yes | -- | -- | -- | -- | -- | -- | -- |
486
- | Response Overrides | Yes | Yes | Yes | Yes | Yes | -- | Yes | -- | -- |
512
+ | Feature | OpenAI Chat | OpenAI Responses | Claude | Gemini | Gemini Int. | Bedrock | Azure | Ollama | Cohere | OpenRouter |
513
+ | -------------------- | ----------- | ---------------- | ------ | ------ | ----------- | ------- | ----- | ------ | ------ | ---------- |
514
+ | Text | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
515
+ | Tool Calls | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
516
+ | Content + Tool Calls | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
517
+ | Streaming | SSE | SSE | SSE | SSE | SSE | Binary | SSE | NDJSON | SSE | SSE |
518
+ | Reasoning | Yes | Yes | Yes | Yes | -- | Yes | Yes | -- | -- | Yes |
519
+ | Web Searches | -- | Yes | -- | -- | -- | -- | -- | -- | -- | -- |
520
+ | Response Overrides | Yes | Yes | Yes | Yes | Yes | -- | Yes | -- | -- | Yes |
487
521
 
488
522
  ## Critical Gotchas
489
523