@copilotkit/aimock 1.22.0 → 1.23.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 (189) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +37 -0
  4. package/README.md +10 -10
  5. package/dist/agui-types.d.ts.map +1 -1
  6. package/dist/aimock-cli.cjs +0 -0
  7. package/dist/aimock-cli.js +0 -0
  8. package/dist/bedrock-converse.cjs +62 -22
  9. package/dist/bedrock-converse.cjs.map +1 -1
  10. package/dist/bedrock-converse.d.cts.map +1 -1
  11. package/dist/bedrock-converse.d.ts.map +1 -1
  12. package/dist/bedrock-converse.js +62 -22
  13. package/dist/bedrock-converse.js.map +1 -1
  14. package/dist/bedrock.cjs +59 -20
  15. package/dist/bedrock.cjs.map +1 -1
  16. package/dist/bedrock.d.cts.map +1 -1
  17. package/dist/bedrock.d.ts.map +1 -1
  18. package/dist/bedrock.js +59 -20
  19. package/dist/bedrock.js.map +1 -1
  20. package/dist/cli.cjs +8 -2
  21. package/dist/cli.cjs.map +1 -1
  22. package/dist/cli.js +8 -2
  23. package/dist/cli.js.map +1 -1
  24. package/dist/cohere.cjs +29 -9
  25. package/dist/cohere.cjs.map +1 -1
  26. package/dist/cohere.d.cts.map +1 -1
  27. package/dist/cohere.d.ts.map +1 -1
  28. package/dist/cohere.js +30 -10
  29. package/dist/cohere.js.map +1 -1
  30. package/dist/config-loader.d.cts.map +1 -1
  31. package/dist/constants.cjs +8 -0
  32. package/dist/constants.cjs.map +1 -0
  33. package/dist/constants.d.cts +8 -0
  34. package/dist/constants.d.cts.map +1 -0
  35. package/dist/constants.d.ts +8 -0
  36. package/dist/constants.d.ts.map +1 -0
  37. package/dist/constants.js +7 -0
  38. package/dist/constants.js.map +1 -0
  39. package/dist/elevenlabs-audio.cjs +41 -18
  40. package/dist/elevenlabs-audio.cjs.map +1 -1
  41. package/dist/elevenlabs-audio.d.cts.map +1 -1
  42. package/dist/elevenlabs-audio.d.ts.map +1 -1
  43. package/dist/elevenlabs-audio.js +42 -19
  44. package/dist/elevenlabs-audio.js.map +1 -1
  45. package/dist/embeddings.cjs +19 -17
  46. package/dist/embeddings.cjs.map +1 -1
  47. package/dist/embeddings.d.cts.map +1 -1
  48. package/dist/embeddings.d.ts.map +1 -1
  49. package/dist/embeddings.js +20 -18
  50. package/dist/embeddings.js.map +1 -1
  51. package/dist/fal-audio.cjs +128 -39
  52. package/dist/fal-audio.cjs.map +1 -1
  53. package/dist/fal-audio.d.cts.map +1 -1
  54. package/dist/fal-audio.d.ts.map +1 -1
  55. package/dist/fal-audio.js +129 -40
  56. package/dist/fal-audio.js.map +1 -1
  57. package/dist/fal.cjs +25 -8
  58. package/dist/fal.cjs.map +1 -1
  59. package/dist/fal.d.cts.map +1 -1
  60. package/dist/fal.d.ts.map +1 -1
  61. package/dist/fal.js +26 -9
  62. package/dist/fal.js.map +1 -1
  63. package/dist/fixture-loader.cjs +11 -2
  64. package/dist/fixture-loader.cjs.map +1 -1
  65. package/dist/fixture-loader.d.cts.map +1 -1
  66. package/dist/fixture-loader.d.ts.map +1 -1
  67. package/dist/fixture-loader.js +11 -2
  68. package/dist/fixture-loader.js.map +1 -1
  69. package/dist/gemini-interactions.cjs +29 -7
  70. package/dist/gemini-interactions.cjs.map +1 -1
  71. package/dist/gemini-interactions.js +28 -8
  72. package/dist/gemini-interactions.js.map +1 -1
  73. package/dist/gemini.cjs +45 -19
  74. package/dist/gemini.cjs.map +1 -1
  75. package/dist/gemini.d.cts.map +1 -1
  76. package/dist/gemini.d.ts.map +1 -1
  77. package/dist/gemini.js +45 -19
  78. package/dist/gemini.js.map +1 -1
  79. package/dist/helpers.cjs +52 -8
  80. package/dist/helpers.cjs.map +1 -1
  81. package/dist/helpers.d.cts +6 -0
  82. package/dist/helpers.d.cts.map +1 -1
  83. package/dist/helpers.d.ts +6 -0
  84. package/dist/helpers.d.ts.map +1 -1
  85. package/dist/helpers.js +52 -9
  86. package/dist/helpers.js.map +1 -1
  87. package/dist/images.cjs +26 -8
  88. package/dist/images.cjs.map +1 -1
  89. package/dist/images.d.cts.map +1 -1
  90. package/dist/images.d.ts.map +1 -1
  91. package/dist/images.js +27 -9
  92. package/dist/images.js.map +1 -1
  93. package/dist/index.cjs +2 -1
  94. package/dist/index.d.cts +2 -1
  95. package/dist/index.d.ts +2 -1
  96. package/dist/index.js +2 -1
  97. package/dist/journal.cjs +17 -7
  98. package/dist/journal.cjs.map +1 -1
  99. package/dist/journal.d.cts +2 -3
  100. package/dist/journal.d.cts.map +1 -1
  101. package/dist/journal.d.ts +2 -3
  102. package/dist/journal.d.ts.map +1 -1
  103. package/dist/journal.js +15 -4
  104. package/dist/journal.js.map +1 -1
  105. package/dist/messages.cjs +33 -12
  106. package/dist/messages.cjs.map +1 -1
  107. package/dist/messages.d.cts.map +1 -1
  108. package/dist/messages.d.ts.map +1 -1
  109. package/dist/messages.js +33 -12
  110. package/dist/messages.js.map +1 -1
  111. package/dist/model-utils.cjs +11 -0
  112. package/dist/model-utils.cjs.map +1 -0
  113. package/dist/model-utils.js +10 -0
  114. package/dist/model-utils.js.map +1 -0
  115. package/dist/ollama.cjs +59 -18
  116. package/dist/ollama.cjs.map +1 -1
  117. package/dist/ollama.d.cts.map +1 -1
  118. package/dist/ollama.d.ts.map +1 -1
  119. package/dist/ollama.js +60 -19
  120. package/dist/ollama.js.map +1 -1
  121. package/dist/recorder.cjs +30 -11
  122. package/dist/recorder.cjs.map +1 -1
  123. package/dist/recorder.d.cts.map +1 -1
  124. package/dist/recorder.d.ts.map +1 -1
  125. package/dist/recorder.js +30 -11
  126. package/dist/recorder.js.map +1 -1
  127. package/dist/responses.cjs +61 -52
  128. package/dist/responses.cjs.map +1 -1
  129. package/dist/responses.d.cts +1 -1
  130. package/dist/responses.d.cts.map +1 -1
  131. package/dist/responses.d.ts +1 -1
  132. package/dist/responses.d.ts.map +1 -1
  133. package/dist/responses.js +62 -53
  134. package/dist/responses.js.map +1 -1
  135. package/dist/router.cjs +7 -3
  136. package/dist/router.cjs.map +1 -1
  137. package/dist/router.js +7 -3
  138. package/dist/router.js.map +1 -1
  139. package/dist/server.cjs +64 -180
  140. package/dist/server.cjs.map +1 -1
  141. package/dist/server.d.cts.map +1 -1
  142. package/dist/server.d.ts.map +1 -1
  143. package/dist/server.js +40 -156
  144. package/dist/server.js.map +1 -1
  145. package/dist/speech.cjs +26 -8
  146. package/dist/speech.cjs.map +1 -1
  147. package/dist/speech.d.cts.map +1 -1
  148. package/dist/speech.d.ts.map +1 -1
  149. package/dist/speech.js +27 -9
  150. package/dist/speech.js.map +1 -1
  151. package/dist/transcription.cjs +57 -19
  152. package/dist/transcription.cjs.map +1 -1
  153. package/dist/transcription.d.cts.map +1 -1
  154. package/dist/transcription.d.ts.map +1 -1
  155. package/dist/transcription.js +58 -20
  156. package/dist/transcription.js.map +1 -1
  157. package/dist/types.d.cts +19 -2
  158. package/dist/types.d.cts.map +1 -1
  159. package/dist/types.d.ts +19 -2
  160. package/dist/types.d.ts.map +1 -1
  161. package/dist/video.cjs +50 -14
  162. package/dist/video.cjs.map +1 -1
  163. package/dist/video.d.cts +8 -1
  164. package/dist/video.d.cts.map +1 -1
  165. package/dist/video.d.ts +8 -1
  166. package/dist/video.d.ts.map +1 -1
  167. package/dist/video.js +51 -15
  168. package/dist/video.js.map +1 -1
  169. package/dist/ws-gemini-live.cjs +34 -27
  170. package/dist/ws-gemini-live.cjs.map +1 -1
  171. package/dist/ws-gemini-live.d.cts +2 -2
  172. package/dist/ws-gemini-live.d.cts.map +1 -1
  173. package/dist/ws-gemini-live.d.ts.map +1 -1
  174. package/dist/ws-gemini-live.js +34 -27
  175. package/dist/ws-gemini-live.js.map +1 -1
  176. package/dist/ws-realtime.cjs +776 -175
  177. package/dist/ws-realtime.cjs.map +1 -1
  178. package/dist/ws-realtime.d.cts +2 -2
  179. package/dist/ws-realtime.d.cts.map +1 -1
  180. package/dist/ws-realtime.d.ts.map +1 -1
  181. package/dist/ws-realtime.js +776 -175
  182. package/dist/ws-realtime.js.map +1 -1
  183. package/dist/ws-responses.cjs +48 -12
  184. package/dist/ws-responses.cjs.map +1 -1
  185. package/dist/ws-responses.d.cts.map +1 -1
  186. package/dist/ws-responses.d.ts.map +1 -1
  187. package/dist/ws-responses.js +49 -13
  188. package/dist/ws-responses.js.map +1 -1
  189. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"ws-realtime.js","names":[],"sources":["../src/ws-realtime.ts"],"sourcesContent":["/**\n * WebSocket handler for OpenAI Realtime API.\n *\n * Accepts Realtime API messages (session.update, conversation.item.create,\n * response.create) over WebSocket and sends back Realtime API events as\n * individual WebSocket text frames.\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport type { ChatCompletionRequest, ChatMessage, Fixture } from \"./types.js\";\nimport { matchFixture } from \"./router.js\";\nimport {\n generateToolCallId,\n isTextResponse,\n isToolCallResponse,\n isErrorResponse,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n} from \"./helpers.js\";\nimport { createInterruptionSignal } from \"./interruption.js\";\nimport { delay } 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\n/** Generate a Realtime-API-style ID with underscore separator (e.g. event_xxx, item_xxx). */\nfunction realtimeId(prefix: string): string {\n return `${prefix}_${randomBytes(12).toString(\"base64url\")}`;\n}\n\n// ─── Realtime protocol types ────────────────────────────────────────────────\n\ninterface RealtimeItem {\n type: \"message\" | \"function_call\" | \"function_call_output\";\n id?: string;\n role?: \"user\" | \"assistant\" | \"system\";\n content?: Array<{ type: string; text?: string }>;\n name?: string;\n call_id?: string;\n arguments?: string;\n output?: string;\n}\n\ninterface SessionConfig {\n model: string;\n modalities: string[];\n instructions: string;\n tools: unknown[];\n voice: string | null;\n input_audio_format: string | null;\n output_audio_format: string | null;\n turn_detection: unknown | null;\n temperature: number;\n}\n\ninterface RealtimeMessage {\n type: string;\n event_id?: string;\n session?: Partial<SessionConfig>;\n item?: RealtimeItem;\n response?: {\n modalities?: string[];\n instructions?: string;\n [key: string]: unknown;\n };\n}\n\n// ─── Conversion helpers ─────────────────────────────────────────────────────\n\nexport function realtimeItemsToMessages(\n items: RealtimeItem[],\n instructions?: string,\n logger?: Logger,\n): ChatMessage[] {\n const messages: ChatMessage[] = [];\n\n if (instructions) {\n messages.push({ role: \"system\", content: instructions });\n }\n\n for (const item of items) {\n if (item.type === \"message\") {\n const text = item.content?.[0]?.text ?? \"\";\n const role =\n item.role === \"assistant\" ? \"assistant\" : item.role === \"system\" ? \"system\" : \"user\";\n messages.push({ role, content: text });\n } else if (item.type === \"function_call\") {\n if (!item.name) {\n logger?.warn(\"Realtime function_call item missing 'name'\");\n }\n messages.push({\n role: \"assistant\",\n content: null,\n tool_calls: [\n {\n id: item.call_id ?? generateToolCallId(),\n type: \"function\",\n function: {\n name: item.name ?? \"\",\n arguments: item.arguments ?? \"\",\n },\n },\n ],\n });\n } else if (item.type === \"function_call_output\") {\n if (!item.output) {\n logger?.warn(\"Realtime function_call_output item missing 'output'\");\n }\n messages.push({\n role: \"tool\",\n content: item.output ?? \"\",\n tool_call_id: item.call_id,\n });\n }\n }\n\n return messages;\n}\n\n// ─── Event builders ─────────────────────────────────────────────────────────\n\nfunction evt(type: string, extra: Record<string, unknown> = {}): string {\n return JSON.stringify({ type, event_id: realtimeId(\"event\"), ...extra });\n}\n\nfunction buildErrorRealtimeEvent(\n message: string,\n type = \"invalid_request_error\",\n code?: string,\n): string {\n return evt(\"error\", { error: { message, type, code } });\n}\n\n// ─── Main handler ───────────────────────────────────────────────────────────\n\nexport function handleWebSocketRealtime(\n ws: WebSocketConnection,\n fixtures: Fixture[],\n journal: Journal,\n defaults: {\n latency: number;\n chunkSize: 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 const sessionId = realtimeId(\"sess\");\n\n const session: SessionConfig = {\n model: defaults.model,\n modalities: [\"text\"],\n instructions: \"\",\n tools: [],\n voice: null,\n input_audio_format: null,\n output_audio_format: null,\n turn_detection: null,\n temperature: 0.8,\n };\n\n const conversationItems: RealtimeItem[] = [];\n\n // Send session.created immediately on connect\n ws.send(\n evt(\"session.created\", {\n session: {\n id: sessionId,\n object: \"realtime.session\",\n ...session,\n expires_at: Math.floor(Date.now() / 1000) + 3600,\n max_response_output_tokens: \"inf\",\n input_audio_transcription: null,\n tool_choice: \"auto\",\n },\n }),\n );\n\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, session, conversationItems).catch(\n (err: unknown) => {\n const msg = err instanceof Error ? err.message : \"Internal error\";\n logger.error(`WebSocket realtime error: ${msg}`);\n try {\n ws.send(buildErrorRealtimeEvent(msg, \"server_error\"));\n } catch {\n // Connection already gone — original error already logged above\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 model: string;\n logger: Logger;\n strict?: boolean;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n testId?: string;\n upgradeHeaders?: import(\"node:http\").IncomingHttpHeaders;\n },\n session: SessionConfig,\n conversationItems: RealtimeItem[],\n): Promise<void> {\n let parsed: RealtimeMessage;\n try {\n parsed = JSON.parse(raw) as RealtimeMessage;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n ws.send(\n buildErrorRealtimeEvent(`Malformed JSON: ${detail}`, \"invalid_request_error\", \"invalid_json\"),\n );\n return;\n }\n\n const msgType = parsed.type;\n\n // ── session.update ────────────────────────────────────────────────────\n if (msgType === \"session.update\") {\n if (parsed.session) {\n if (parsed.session.instructions !== undefined) {\n session.instructions = parsed.session.instructions;\n }\n if (parsed.session.tools !== undefined) {\n session.tools = parsed.session.tools;\n }\n if (parsed.session.modalities !== undefined) {\n session.modalities = parsed.session.modalities;\n }\n if (parsed.session.model !== undefined) {\n session.model = parsed.session.model;\n }\n if (parsed.session.temperature !== undefined) {\n session.temperature = parsed.session.temperature;\n }\n }\n ws.send(\n evt(\"session.updated\", {\n session: {\n ...session,\n object: \"realtime.session\",\n expires_at: Math.floor(Date.now() / 1000) + 3600,\n max_response_output_tokens: \"inf\",\n input_audio_transcription: null,\n tool_choice: \"auto\",\n },\n }),\n );\n return;\n }\n\n // ── conversation.item.create ──────────────────────────────────────────\n if (msgType === \"conversation.item.create\") {\n if (!parsed.item) {\n ws.send(\n buildErrorRealtimeEvent(\n \"Missing 'item' in conversation.item.create\",\n \"invalid_request_error\",\n ),\n );\n return;\n }\n const item = parsed.item;\n if (!item.id) {\n item.id = realtimeId(\"item\");\n }\n const previousId =\n conversationItems.length > 0\n ? (conversationItems[conversationItems.length - 1].id ?? null)\n : null;\n conversationItems.push(item);\n ws.send(evt(\"conversation.item.created\", { previous_item_id: previousId, item }));\n return;\n }\n\n // ── response.create ───────────────────────────────────────────────────\n if (msgType === \"response.create\") {\n await handleResponseCreate(ws, fixtures, journal, defaults, session, conversationItems);\n return;\n }\n\n // Unknown message type — ignore silently (matches OpenAI behavior)\n}\n\nasync function handleResponseCreate(\n ws: WebSocketConnection,\n fixtures: Fixture[],\n journal: Journal,\n defaults: {\n latency: number;\n chunkSize: 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 session: SessionConfig,\n conversationItems: RealtimeItem[],\n): Promise<void> {\n const instructions = session.instructions || undefined;\n const messages = realtimeItemsToMessages(conversationItems, instructions, defaults.logger);\n\n const completionReq: ChatCompletionRequest = {\n model: session.model,\n messages,\n };\n\n const testId = defaults.testId ?? DEFAULT_TEST_ID;\n const fixture = matchFixture(\n fixtures,\n completionReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n const responseId = realtimeId(\"resp\");\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n }\n\n if (!fixture) {\n if (resolveStrictMode(defaults.strict, defaults.upgradeHeaders)) {\n defaults.logger.warn(`STRICT: No fixture matched for WebSocket message`);\n ws.close(1008, \"Strict mode: no fixture matched\");\n return;\n }\n journal.add({\n method: \"WS\",\n path: \"/v1/realtime\",\n headers: {},\n body: completionReq,\n response: {\n status: 404,\n fixture: null,\n ...strictOverrideField(defaults.strict, defaults.upgradeHeaders),\n },\n });\n // Send response.created with failed status then response.done with error\n ws.send(\n evt(\"response.created\", {\n response: {\n id: responseId,\n object: \"realtime.response\",\n status: \"failed\",\n status_details: null,\n output: [],\n usage: null,\n },\n }),\n );\n ws.send(\n evt(\"response.done\", {\n response: {\n id: responseId,\n object: \"realtime.response\",\n status: \"failed\",\n output: [],\n status_details: {\n type: \"error\",\n error: {\n message: \"No fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n },\n usage: { total_tokens: 0, input_tokens: 0, output_tokens: 0 },\n },\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 // ── Error fixture ───────────────────────────────────────────────────\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: \"WS\",\n path: \"/v1/realtime\",\n headers: {},\n body: completionReq,\n response: { status, fixture },\n });\n ws.send(\n evt(\"response.created\", {\n response: {\n id: responseId,\n object: \"realtime.response\",\n status: \"failed\",\n status_details: null,\n output: [],\n usage: null,\n },\n }),\n );\n ws.send(\n evt(\"response.done\", {\n response: {\n id: responseId,\n object: \"realtime.response\",\n status: \"failed\",\n output: [],\n status_details: {\n type: \"error\",\n error: {\n message: response.error.message,\n type: response.error.type,\n code: response.error.code,\n },\n },\n usage: { total_tokens: 0, input_tokens: 0, output_tokens: 0 },\n },\n }),\n );\n return;\n }\n\n // ── Text response ───────────────────────────────────────────────────\n if (isTextResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path: \"/v1/realtime\",\n headers: {},\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n const itemId = realtimeId(\"item\");\n const contentIndex = 0;\n const outputIndex = 0;\n\n const outputItem = {\n id: itemId,\n type: \"message\",\n role: \"assistant\",\n status: \"completed\",\n content: [{ type: \"text\", text: response.content }],\n };\n\n // response.created\n ws.send(\n evt(\"response.created\", {\n response: {\n id: responseId,\n object: \"realtime.response\",\n status: \"in_progress\",\n status_details: null,\n output: [],\n usage: null,\n },\n }),\n );\n\n // response.output_item.added\n ws.send(\n evt(\"response.output_item.added\", {\n response_id: responseId,\n output_index: outputIndex,\n item: {\n id: itemId,\n type: \"message\",\n role: \"assistant\",\n status: \"in_progress\",\n content: [],\n },\n }),\n );\n\n // response.content_part.added\n ws.send(\n evt(\"response.content_part.added\", {\n response_id: responseId,\n item_id: itemId,\n output_index: outputIndex,\n content_index: contentIndex,\n part: { type: \"text\", text: \"\" },\n }),\n );\n\n // response.text.delta (chunked)\n const content = response.content;\n const interruption = createInterruptionSignal(fixture);\n let interrupted = false;\n\n for (let i = 0; i < content.length; i += chunkSize) {\n if (ws.isClosed) break;\n if (latency > 0) await delay(latency, interruption?.signal);\n if (interruption?.signal.aborted) {\n interrupted = true;\n break;\n }\n if (ws.isClosed) break;\n const chunk = content.slice(i, i + chunkSize);\n ws.send(\n evt(\"response.text.delta\", {\n response_id: responseId,\n item_id: itemId,\n output_index: outputIndex,\n content_index: contentIndex,\n delta: chunk,\n }),\n );\n interruption?.tick();\n if (interruption?.signal.aborted) {\n interrupted = true;\n break;\n }\n }\n\n if (interrupted) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n interruption?.cleanup();\n return;\n }\n\n interruption?.cleanup();\n\n if (ws.isClosed) return;\n\n // response.text.done\n ws.send(\n evt(\"response.text.done\", {\n response_id: responseId,\n item_id: itemId,\n output_index: outputIndex,\n content_index: contentIndex,\n text: content,\n }),\n );\n\n // response.content_part.done\n ws.send(\n evt(\"response.content_part.done\", {\n response_id: responseId,\n item_id: itemId,\n output_index: outputIndex,\n content_index: contentIndex,\n part: { type: \"text\", text: content },\n }),\n );\n\n // response.output_item.done\n ws.send(\n evt(\"response.output_item.done\", {\n response_id: responseId,\n output_index: outputIndex,\n item: outputItem,\n }),\n );\n\n // response.done\n ws.send(\n evt(\"response.done\", {\n response: {\n id: responseId,\n object: \"realtime.response\",\n status: \"completed\",\n output: [outputItem],\n usage: { total_tokens: 0, input_tokens: 0, output_tokens: 0 },\n },\n }),\n );\n\n // Accumulate assistant response into conversation for multi-turn\n conversationItems.push({\n type: \"message\",\n id: itemId,\n role: \"assistant\",\n content: [{ type: \"text\", text: content }],\n });\n return;\n }\n\n // ── Tool call response ──────────────────────────────────────────────\n if (isToolCallResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path: \"/v1/realtime\",\n headers: {},\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n // response.created\n ws.send(\n evt(\"response.created\", {\n response: {\n id: responseId,\n object: \"realtime.response\",\n status: \"in_progress\",\n status_details: null,\n output: [],\n usage: null,\n },\n }),\n );\n\n const outputItems: unknown[] = [];\n const interruption = createInterruptionSignal(fixture);\n let interrupted = false;\n\n for (let tcIdx = 0; tcIdx < response.toolCalls.length; tcIdx++) {\n const tc = response.toolCalls[tcIdx];\n const callId = tc.id ?? generateToolCallId();\n const itemId = realtimeId(\"item\");\n\n const outputItem = {\n id: itemId,\n type: \"function_call\",\n status: \"completed\",\n call_id: callId,\n name: tc.name,\n arguments: tc.arguments,\n };\n\n // response.output_item.added\n ws.send(\n evt(\"response.output_item.added\", {\n response_id: responseId,\n output_index: tcIdx,\n item: {\n id: itemId,\n type: \"function_call\",\n status: \"in_progress\",\n call_id: callId,\n name: tc.name,\n arguments: \"\",\n },\n }),\n );\n\n // response.function_call_arguments.delta (chunked)\n const args = tc.arguments;\n for (let i = 0; i < args.length; i += chunkSize) {\n if (ws.isClosed) break;\n if (latency > 0) await delay(latency, interruption?.signal);\n if (interruption?.signal.aborted) {\n interrupted = true;\n break;\n }\n if (ws.isClosed) break;\n const chunk = args.slice(i, i + chunkSize);\n ws.send(\n evt(\"response.function_call_arguments.delta\", {\n response_id: responseId,\n item_id: itemId,\n output_index: tcIdx,\n call_id: callId,\n delta: chunk,\n }),\n );\n interruption?.tick();\n if (interruption?.signal.aborted) {\n interrupted = true;\n break;\n }\n }\n\n if (interrupted) break;\n\n // response.function_call_arguments.done\n ws.send(\n evt(\"response.function_call_arguments.done\", {\n response_id: responseId,\n item_id: itemId,\n output_index: tcIdx,\n call_id: callId,\n arguments: args,\n }),\n );\n\n // response.output_item.done\n ws.send(\n evt(\"response.output_item.done\", {\n response_id: responseId,\n output_index: tcIdx,\n item: outputItem,\n }),\n );\n\n outputItems.push(outputItem);\n }\n\n if (interrupted) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n interruption?.cleanup();\n return;\n }\n\n interruption?.cleanup();\n\n if (ws.isClosed) return;\n\n // response.done\n ws.send(\n evt(\"response.done\", {\n response: {\n id: responseId,\n object: \"realtime.response\",\n status: \"completed\",\n output: outputItems,\n usage: { total_tokens: 0, input_tokens: 0, output_tokens: 0 },\n },\n }),\n );\n\n // Accumulate assistant tool calls into conversation for multi-turn\n // Reuse outputItems (which already have the correct call_id) to avoid generating divergent IDs\n for (const item of outputItems) {\n conversationItems.push(item as RealtimeItem);\n }\n return;\n }\n\n // Unknown response type\n journal.add({\n method: \"WS\",\n path: \"/v1/realtime\",\n headers: {},\n body: completionReq,\n response: { status: 500, fixture },\n });\n ws.send(buildErrorRealtimeEvent(\"Fixture response did not match any known type\", \"server_error\"));\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ws-realtime.js","names":[],"sources":["../src/ws-realtime.ts"],"sourcesContent":["/**\n * WebSocket handler for OpenAI Realtime API.\n *\n * Accepts Realtime API messages (session.update, conversation.item.create,\n * response.create) over WebSocket and sends back Realtime API events as\n * individual WebSocket text frames.\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport type { ChatCompletionRequest, ChatMessage, Fixture } from \"./types.js\";\nimport { matchFixture } from \"./router.js\";\nimport {\n generateToolCallId,\n flattenHeaders,\n isTextResponse,\n isToolCallResponse,\n isContentWithToolCallsResponse,\n isErrorResponse,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n} from \"./helpers.js\";\nimport { createInterruptionSignal } from \"./interruption.js\";\nimport { delay } 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\n/** Generate a Realtime-API-style ID with underscore separator (e.g. event_xxx, item_xxx). */\nfunction realtimeId(prefix: string): string {\n return `${prefix}_${randomBytes(12).toString(\"base64url\")}`;\n}\n\n// ─── Realtime protocol types ────────────────────────────────────────────────\n\ninterface RealtimeItem {\n type: \"message\" | \"function_call\" | \"function_call_output\";\n id?: string;\n role?: \"user\" | \"assistant\" | \"system\";\n content?: Array<{ type: string; text?: string; url?: string; transcript?: string | null }>;\n name?: string;\n call_id?: string;\n arguments?: string;\n output?: string;\n}\n\ninterface SessionConfig {\n model: string;\n modalities: string[];\n instructions: string;\n tools: unknown[];\n voice: string | null;\n input_audio_format: string | null;\n output_audio_format: string | null;\n input_audio_noise_reduction: { type: string } | null;\n input_audio_transcription: { model: string } | null;\n turn_detection: unknown | null;\n temperature: number;\n type: \"conversation\" | \"transcription\" | \"translation\";\n reasoning: { effort: string } | null;\n}\n\ninterface RealtimeMessage {\n type: string;\n event_id?: string;\n session?: Partial<SessionConfig>;\n item?: RealtimeItem;\n response?: {\n modalities?: string[];\n instructions?: string;\n [key: string]: unknown;\n };\n}\n\n// ─── Conversion helpers ─────────────────────────────────────────────────────\n\nexport function realtimeItemsToMessages(\n items: RealtimeItem[],\n instructions?: string,\n logger?: Logger,\n): ChatMessage[] {\n const messages: ChatMessage[] = [];\n\n if (instructions) {\n messages.push({ role: \"system\", content: instructions });\n }\n\n for (const item of items) {\n if (item.type === \"message\") {\n const role =\n item.role === \"assistant\" ? \"assistant\" : item.role === \"system\" ? \"system\" : \"user\";\n\n // Check if content contains multimodal input types (input_text, input_image, input_audio)\n const hasMultimodal = item.content?.some(\n (p) => p.type === \"input_text\" || p.type === \"input_image\" || p.type === \"input_audio\",\n );\n\n if (hasMultimodal && item.content) {\n // Map realtime input content types to ChatMessage content parts\n const mappedContent = item.content.map((part) => {\n if (part.type === \"input_text\") {\n return { type: \"text\" as const, text: part.text ?? \"\" };\n }\n if (part.type === \"input_image\") {\n return {\n type: \"image_url\" as const,\n image_url: { url: part.url ?? \"\" },\n };\n }\n if (part.type === \"input_audio\") {\n return { type: \"text\" as const, text: \"[audio input]\" };\n }\n // Pass through unknown content types as-is\n return part;\n });\n messages.push({ role, content: mappedContent });\n } else {\n // Existing behavior: extract text from first content element\n const text = item.content?.[0]?.text ?? \"\";\n messages.push({ role, content: text });\n }\n } else if (item.type === \"function_call\") {\n if (!item.name) {\n logger?.warn(\"Realtime function_call item missing 'name'\");\n }\n messages.push({\n role: \"assistant\",\n content: null,\n tool_calls: [\n {\n id: item.call_id ?? generateToolCallId(),\n type: \"function\",\n function: {\n name: item.name ?? \"\",\n arguments: item.arguments ?? \"\",\n },\n },\n ],\n });\n } else if (item.type === \"function_call_output\") {\n if (!item.output) {\n logger?.warn(\"Realtime function_call_output item missing 'output'\");\n }\n messages.push({\n role: \"tool\",\n content: item.output ?? \"\",\n tool_call_id: item.call_id,\n });\n }\n }\n\n return messages;\n}\n\n// ─── GA -> Beta translation ─────────────────────────────────────────────────\n\n/** GA -> Beta event name mapping */\nconst GA_TO_BETA_EVENT: Record<string, string> = {\n \"response.output_text.delta\": \"response.text.delta\",\n \"response.output_text.done\": \"response.text.done\",\n \"response.output_audio.delta\": \"response.audio.delta\",\n \"response.output_audio.done\": \"response.audio.done\",\n \"response.output_audio_transcript.delta\": \"response.audio_transcript.delta\",\n \"response.output_audio_transcript.done\": \"response.audio_transcript.done\",\n \"conversation.item.added\": \"conversation.item.created\",\n};\n\n/** GA -> Beta content type mapping */\nconst GA_TO_BETA_CONTENT_TYPE: Record<string, string> = {\n output_text: \"text\",\n output_audio: \"audio\",\n};\n\n/** Events suppressed in Beta mode (GA-only events) */\nconst BETA_SUPPRESSED_EVENTS = new Set([\"conversation.item.done\"]);\n\nfunction translateGAToBeta(event: Record<string, unknown>): Record<string, unknown> | null {\n const type = event.type as string;\n if (BETA_SUPPRESSED_EVENTS.has(type)) return null;\n\n const translated = { ...event };\n if (GA_TO_BETA_EVENT[type]) {\n translated.type = GA_TO_BETA_EVENT[type];\n }\n\n // Translate content types in nested structures\n if (translated.part && typeof translated.part === \"object\") {\n const part = { ...(translated.part as Record<string, unknown>) };\n if (typeof part.type === \"string\" && GA_TO_BETA_CONTENT_TYPE[part.type]) {\n part.type = GA_TO_BETA_CONTENT_TYPE[part.type];\n }\n translated.part = part;\n }\n if (translated.content_part && typeof translated.content_part === \"object\") {\n const cp = { ...(translated.content_part as Record<string, unknown>) };\n if (typeof cp.type === \"string\" && GA_TO_BETA_CONTENT_TYPE[cp.type]) {\n cp.type = GA_TO_BETA_CONTENT_TYPE[cp.type];\n }\n translated.content_part = cp;\n }\n // Translate content arrays\n if (Array.isArray(translated.content)) {\n translated.content = (translated.content as Record<string, unknown>[]).map((c) => {\n if (typeof c.type === \"string\" && GA_TO_BETA_CONTENT_TYPE[c.type]) {\n return { ...c, type: GA_TO_BETA_CONTENT_TYPE[c.type] };\n }\n return c;\n });\n }\n // Translate item.content arrays (response.output_item.added/done, conversation.item.added)\n if (translated.item && typeof translated.item === \"object\") {\n const item = { ...(translated.item as Record<string, unknown>) };\n delete item.phase; // GA-only field\n if (Array.isArray(item.content)) {\n item.content = (item.content as Record<string, unknown>[]).map((c) => {\n if (typeof c.type === \"string\" && GA_TO_BETA_CONTENT_TYPE[c.type]) {\n return { ...c, type: GA_TO_BETA_CONTENT_TYPE[c.type] };\n }\n return c;\n });\n }\n translated.item = item;\n }\n // Translate response.output[].content arrays (response.done)\n if (translated.response && typeof translated.response === \"object\") {\n const resp = { ...(translated.response as Record<string, unknown>) };\n if (Array.isArray(resp.output)) {\n resp.output = (resp.output as Record<string, unknown>[]).map((outItem) => {\n const o = { ...(outItem as Record<string, unknown>) };\n if (Array.isArray(o.content)) {\n o.content = (o.content as Record<string, unknown>[]).map((c) =>\n typeof c.type === \"string\" && GA_TO_BETA_CONTENT_TYPE[c.type]\n ? { ...c, type: GA_TO_BETA_CONTENT_TYPE[c.type] }\n : c,\n );\n }\n return o;\n });\n }\n translated.response = resp;\n }\n\n // Flatten GA session config for Beta (session.created / session.updated)\n if (type === \"session.created\" || type === \"session.updated\") {\n if (translated.session && typeof translated.session === \"object\") {\n const session = { ...(translated.session as Record<string, unknown>) };\n if (session.audio && typeof session.audio === \"object\") {\n const audio = session.audio as Record<string, unknown>;\n session.voice = audio.voice;\n session.input_audio_format = audio.input_audio_format;\n session.output_audio_format = audio.output_audio_format;\n session.input_audio_transcription = audio.input_audio_transcription;\n delete session.audio;\n }\n delete session.type;\n delete session.reasoning;\n translated.session = session;\n }\n }\n\n return translated;\n}\n\n// ─── Event sending ──────────────────────────────────────────────────────────\n\nfunction sendEvent(ws: WebSocketConnection, event: Record<string, unknown>, isBeta: boolean): void {\n const out = { ...event, event_id: event.event_id ?? realtimeId(\"event\") };\n if (isBeta) {\n const translated = translateGAToBeta(out);\n if (translated === null) return; // suppressed in Beta mode\n ws.send(JSON.stringify(translated));\n } else {\n ws.send(JSON.stringify(out));\n }\n}\n\nfunction buildErrorRealtimeEvent(\n ws: WebSocketConnection,\n message: string,\n isBeta: boolean,\n type = \"invalid_request_error\",\n code?: string,\n): void {\n sendEvent(ws, { type: \"error\", error: { message, type, code } }, isBeta);\n}\n\n// ─── Main handler ───────────────────────────────────────────────────────────\n\nexport function handleWebSocketRealtime(\n ws: WebSocketConnection,\n fixtures: Fixture[],\n journal: Journal,\n defaults: {\n latency: number;\n chunkSize: 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 const sessionId = realtimeId(\"sess\");\n\n const isBeta = defaults.upgradeHeaders?.[\"openai-beta\"]\n ? String(defaults.upgradeHeaders[\"openai-beta\"]).includes(\"realtime=v1\")\n : false;\n\n const session: SessionConfig = {\n model: defaults.model,\n modalities: [\"text\"],\n instructions: \"\",\n tools: [],\n voice: null,\n input_audio_format: null,\n output_audio_format: null,\n input_audio_noise_reduction: null,\n input_audio_transcription: null,\n turn_detection: null,\n temperature: 0.8,\n type: \"conversation\",\n reasoning: null,\n };\n\n const conversationItems: RealtimeItem[] = [];\n\n // Send session.created immediately on connect (GA format — shim flattens for Beta)\n sendEvent(\n ws,\n {\n type: \"session.created\",\n session: {\n id: sessionId,\n object: \"realtime.session\",\n model: session.model,\n expires_at: Math.floor(Date.now() / 1000) + 3600,\n modalities: session.modalities,\n instructions: session.instructions,\n tools: session.tools,\n tool_choice: \"auto\",\n temperature: session.temperature,\n max_response_output_tokens: \"inf\",\n audio: {\n voice: session.voice,\n input_audio_format: session.input_audio_format,\n output_audio_format: session.output_audio_format,\n input_audio_noise_reduction: session.input_audio_noise_reduction,\n input_audio_transcription: session.input_audio_transcription,\n },\n turn_detection: session.turn_detection,\n type: session.type,\n reasoning: session.reasoning,\n },\n },\n isBeta,\n );\n\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(\n raw,\n ws,\n fixtures,\n journal,\n defaults,\n session,\n conversationItems,\n isBeta,\n ).catch((err: unknown) => {\n const msg = err instanceof Error ? err.message : \"Internal error\";\n logger.error(`WebSocket realtime error: ${msg}`);\n try {\n buildErrorRealtimeEvent(ws, msg, isBeta, \"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 model: string;\n logger: Logger;\n strict?: boolean;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n testId?: string;\n upgradeHeaders?: import(\"node:http\").IncomingHttpHeaders;\n },\n session: SessionConfig,\n conversationItems: RealtimeItem[],\n isBeta: boolean,\n): Promise<void> {\n let parsed: RealtimeMessage;\n try {\n parsed = JSON.parse(raw) as RealtimeMessage;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n buildErrorRealtimeEvent(\n ws,\n `Malformed JSON: ${detail}`,\n isBeta,\n \"invalid_request_error\",\n \"invalid_json\",\n );\n return;\n }\n\n const msgType = parsed.type;\n\n // ── session.update ────────────────────────────────────────────────────\n if (msgType === \"session.update\") {\n if (parsed.session) {\n const s = parsed.session;\n\n // Validate session.type value before applying any mutations\n const validTypes = new Set([\"conversation\", \"transcription\", \"translation\"]);\n if ((s as Record<string, unknown>).type !== undefined) {\n if (!validTypes.has((s as Record<string, unknown>).type as string)) {\n sendEvent(\n ws,\n {\n type: \"error\",\n error: {\n message: `Invalid session type: ${(s as Record<string, unknown>).type}`,\n type: \"invalid_request_error\",\n code: \"invalid_session_config\",\n },\n },\n isBeta,\n );\n return;\n }\n }\n\n // Capture pre-mutation values for rollback on validation failure\n const prevModel = session.model;\n const prevType = session.type;\n\n if (s.instructions !== undefined) session.instructions = s.instructions;\n if (s.tools !== undefined) session.tools = s.tools;\n if (s.modalities !== undefined) session.modalities = s.modalities;\n if (s.model !== undefined) session.model = s.model;\n if (s.temperature !== undefined) session.temperature = s.temperature;\n if ((s as Record<string, unknown>).type !== undefined)\n session.type = (s as Record<string, unknown>).type as SessionConfig[\"type\"];\n // GA nested audio config\n if ((s as Record<string, unknown>).audio) {\n const audio = (s as Record<string, unknown>).audio as Record<string, unknown>;\n if (audio.voice !== undefined) session.voice = audio.voice as string | null;\n if (audio.input_audio_format !== undefined)\n session.input_audio_format = audio.input_audio_format as string | null;\n if (audio.output_audio_format !== undefined)\n session.output_audio_format = audio.output_audio_format as string | null;\n if (audio.input_audio_noise_reduction !== undefined)\n session.input_audio_noise_reduction = audio.input_audio_noise_reduction as {\n type: string;\n } | null;\n if (audio.input_audio_transcription !== undefined)\n session.input_audio_transcription = audio.input_audio_transcription as {\n model: string;\n } | null;\n }\n // Beta flat fields (backward compat)\n if (s.voice !== undefined) session.voice = s.voice;\n if (s.input_audio_format !== undefined) session.input_audio_format = s.input_audio_format;\n if (s.output_audio_format !== undefined) session.output_audio_format = s.output_audio_format;\n // reasoning config\n if ((s as Record<string, unknown>).reasoning !== undefined)\n session.reasoning = (s as Record<string, unknown>).reasoning as {\n effort: string;\n } | null;\n\n // Validate model+type combinations (rollback on failure)\n const transcriptionModels = new Set([\n \"gpt-4o-transcribe\",\n \"gpt-4o-mini-transcribe\",\n \"gpt-realtime-whisper\",\n \"whisper-1\",\n ]);\n const translationModels = new Set([\n \"gpt-4o-transcribe\",\n \"gpt-4o-mini-transcribe\",\n \"gpt-realtime-translate\",\n ]);\n\n if (session.type === \"transcription\" && !transcriptionModels.has(session.model)) {\n session.model = prevModel;\n session.type = prevType;\n sendEvent(\n ws,\n {\n type: \"error\",\n error: {\n message: `Model ${s.model ?? prevModel} does not support session type transcription`,\n type: \"invalid_request_error\",\n code: \"invalid_session_config\",\n },\n },\n isBeta,\n );\n return;\n }\n if (session.type === \"translation\" && !translationModels.has(session.model)) {\n session.model = prevModel;\n session.type = prevType;\n sendEvent(\n ws,\n {\n type: \"error\",\n error: {\n message: `Model ${s.model ?? prevModel} does not support session type translation`,\n type: \"invalid_request_error\",\n code: \"invalid_session_config\",\n },\n },\n isBeta,\n );\n return;\n }\n }\n\n sendEvent(\n ws,\n {\n type: \"session.updated\",\n session: {\n object: \"realtime.session\",\n model: session.model,\n expires_at: Math.floor(Date.now() / 1000) + 3600,\n modalities: session.modalities,\n instructions: session.instructions,\n tools: session.tools,\n tool_choice: \"auto\",\n temperature: session.temperature,\n max_response_output_tokens: \"inf\",\n audio: {\n voice: session.voice,\n input_audio_format: session.input_audio_format,\n output_audio_format: session.output_audio_format,\n input_audio_noise_reduction: session.input_audio_noise_reduction,\n input_audio_transcription: session.input_audio_transcription,\n },\n turn_detection: session.turn_detection,\n type: session.type,\n reasoning: session.reasoning,\n },\n },\n isBeta,\n );\n return;\n }\n\n // ── conversation.item.create ──────────────────────────────────────────\n if (msgType === \"conversation.item.create\") {\n if (!parsed.item) {\n buildErrorRealtimeEvent(\n ws,\n \"Missing 'item' in conversation.item.create\",\n isBeta,\n \"invalid_request_error\",\n );\n return;\n }\n const item = parsed.item;\n if (!item.id) {\n item.id = realtimeId(\"item\");\n }\n const previousId =\n conversationItems.length > 0\n ? (conversationItems[conversationItems.length - 1].id ?? null)\n : null;\n conversationItems.push(item);\n sendEvent(ws, { type: \"conversation.item.added\", previous_item_id: previousId, item }, isBeta);\n return;\n }\n\n // ── response.create ───────────────────────────────────────────────────\n if (msgType === \"response.create\") {\n await handleResponseCreate(\n ws,\n fixtures,\n journal,\n defaults,\n session,\n conversationItems,\n isBeta,\n parsed.response,\n );\n return;\n }\n\n // ── input_audio_buffer.append ────────────────────────────────────────\n if (msgType === \"input_audio_buffer.append\") {\n // Accept silently — aimock doesn't process actual audio\n return;\n }\n\n // ── input_audio_buffer.commit ──────────────────────────────────────\n if (msgType === \"input_audio_buffer.commit\") {\n sendEvent(ws, { type: \"input_audio_buffer.committed\" }, isBeta);\n // In transcription/translation mode, add a placeholder user item\n if (session.type === \"transcription\" || session.type === \"translation\") {\n const audioItem: RealtimeItem = {\n type: \"message\",\n id: realtimeId(\"item\"),\n role: \"user\",\n content: [{ type: \"input_audio\", transcript: null }],\n };\n conversationItems.push(audioItem);\n sendEvent(\n ws,\n {\n type: \"conversation.item.added\",\n item: audioItem,\n },\n isBeta,\n );\n }\n return;\n }\n\n // ── input_audio_buffer.clear ───────────────────────────────────────\n if (msgType === \"input_audio_buffer.clear\") {\n sendEvent(ws, { type: \"input_audio_buffer.cleared\" }, isBeta);\n return;\n }\n\n // ── response.cancel ────────────────────────────────────────────────\n if (msgType === \"response.cancel\") {\n sendEvent(ws, { type: \"response.cancelled\" }, isBeta);\n return;\n }\n\n // Unknown message type — ignore silently (matches OpenAI behavior)\n}\n\nasync function handleResponseCreate(\n ws: WebSocketConnection,\n fixtures: Fixture[],\n journal: Journal,\n defaults: {\n latency: number;\n chunkSize: 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 session: SessionConfig,\n conversationItems: RealtimeItem[],\n isBeta: boolean,\n responseOverrides?: { instructions?: string; [key: string]: unknown },\n): Promise<void> {\n const instructions = (responseOverrides?.instructions ?? session.instructions) || undefined;\n const messages = realtimeItemsToMessages(conversationItems, instructions, defaults.logger);\n\n const endpointTypeMap: Record<string, string> = {\n conversation: \"realtime\",\n transcription: \"realtime-transcription\",\n translation: \"realtime-translation\",\n };\n const endpointType = endpointTypeMap[session.type] ?? \"realtime\";\n\n const completionReq: ChatCompletionRequest = {\n model: session.model,\n messages,\n _endpointType: endpointType,\n };\n\n const testId = defaults.testId ?? DEFAULT_TEST_ID;\n const fixture = matchFixture(\n fixtures,\n completionReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n const responseId = realtimeId(\"resp\");\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n }\n\n if (!fixture) {\n if (resolveStrictMode(defaults.strict, defaults.upgradeHeaders)) {\n defaults.logger.warn(`STRICT: No fixture matched for WebSocket message`);\n journal.add({\n method: \"WS\",\n path: \"/v1/realtime\",\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, \"Strict mode: no fixture matched\");\n return;\n }\n journal.add({\n method: \"WS\",\n path: \"/v1/realtime\",\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 // Send response.created with failed status then response.done with error\n sendEvent(\n ws,\n {\n type: \"response.created\",\n response: {\n id: responseId,\n object: \"realtime.response\",\n status: \"failed\",\n status_details: null,\n output: [],\n usage: null,\n },\n },\n isBeta,\n );\n sendEvent(\n ws,\n {\n type: \"response.done\",\n response: {\n id: responseId,\n object: \"realtime.response\",\n status: \"failed\",\n output: [],\n status_details: {\n type: \"error\",\n error: {\n message: \"No fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n },\n usage: { total_tokens: 0, input_tokens: 0, output_tokens: 0 },\n },\n },\n isBeta,\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 // ── Error fixture ───────────────────────────────────────────────────\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: \"WS\",\n path: \"/v1/realtime\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status, fixture },\n });\n sendEvent(\n ws,\n {\n type: \"response.created\",\n response: {\n id: responseId,\n object: \"realtime.response\",\n status: \"failed\",\n status_details: null,\n output: [],\n usage: null,\n },\n },\n isBeta,\n );\n sendEvent(\n ws,\n {\n type: \"response.done\",\n response: {\n id: responseId,\n object: \"realtime.response\",\n status: \"failed\",\n output: [],\n status_details: {\n type: \"error\",\n error: {\n message: response.error.message,\n type: response.error.type,\n code: response.error.code,\n },\n },\n usage: { total_tokens: 0, input_tokens: 0, output_tokens: 0 },\n },\n },\n isBeta,\n );\n return;\n }\n\n // ── Content + tool calls response ──────────────────────────────────\n if (isContentWithToolCallsResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path: \"/v1/realtime\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n // response.created\n sendEvent(\n ws,\n {\n type: \"response.created\",\n response: {\n id: responseId,\n object: \"realtime.response\",\n status: \"in_progress\",\n status_details: null,\n output: [],\n usage: null,\n },\n },\n isBeta,\n );\n\n const interruption = createInterruptionSignal(fixture);\n let interrupted = false;\n const allOutputItems: unknown[] = [];\n\n // ── Text content part ──────────────────────────────────────────\n const textItemId = realtimeId(\"item\");\n const contentIndex = 0;\n const textOutputIndex = 0;\n\n const textOutputItem = {\n id: textItemId,\n type: \"message\",\n role: \"assistant\",\n status: \"completed\",\n content: [{ type: \"output_text\", text: response.content }],\n };\n\n // Determine phase: text is \"commentary\" when tool calls are also present\n const hasToolCalls = response.toolCalls && response.toolCalls.length > 0;\n const textPhase = hasToolCalls ? \"commentary\" : \"final_answer\";\n\n // response.output_item.added (text)\n sendEvent(\n ws,\n {\n type: \"response.output_item.added\",\n response_id: responseId,\n output_index: textOutputIndex,\n item: {\n id: textItemId,\n type: \"message\",\n role: \"assistant\",\n status: \"in_progress\",\n content: [],\n phase: textPhase,\n },\n },\n isBeta,\n );\n\n // response.content_part.added\n sendEvent(\n ws,\n {\n type: \"response.content_part.added\",\n response_id: responseId,\n item_id: textItemId,\n output_index: textOutputIndex,\n content_index: contentIndex,\n part: { type: \"output_text\", text: \"\" },\n },\n isBeta,\n );\n\n // response.output_text.delta (chunked) — GA name\n const content = response.content;\n for (let i = 0; i < content.length; i += chunkSize) {\n if (ws.isClosed) break;\n if (latency > 0) await delay(latency, interruption?.signal);\n if (interruption?.signal.aborted) {\n interrupted = true;\n break;\n }\n if (ws.isClosed) break;\n const chunk = content.slice(i, i + chunkSize);\n sendEvent(\n ws,\n {\n type: \"response.output_text.delta\",\n response_id: responseId,\n item_id: textItemId,\n output_index: textOutputIndex,\n content_index: contentIndex,\n delta: chunk,\n },\n isBeta,\n );\n interruption?.tick();\n if (interruption?.signal.aborted) {\n interrupted = true;\n break;\n }\n }\n\n if (interrupted) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n interruption?.cleanup();\n return;\n }\n\n if (ws.isClosed) {\n interruption?.cleanup();\n return;\n }\n\n // response.output_text.done\n sendEvent(\n ws,\n {\n type: \"response.output_text.done\",\n response_id: responseId,\n item_id: textItemId,\n output_index: textOutputIndex,\n content_index: contentIndex,\n text: content,\n },\n isBeta,\n );\n\n if (ws.isClosed) {\n interruption?.cleanup();\n return;\n }\n\n // response.content_part.done\n sendEvent(\n ws,\n {\n type: \"response.content_part.done\",\n response_id: responseId,\n item_id: textItemId,\n output_index: textOutputIndex,\n content_index: contentIndex,\n part: { type: \"output_text\", text: content },\n },\n isBeta,\n );\n\n if (ws.isClosed) {\n interruption?.cleanup();\n return;\n }\n\n // response.output_item.done (text)\n sendEvent(\n ws,\n {\n type: \"response.output_item.done\",\n response_id: responseId,\n output_index: textOutputIndex,\n item: { ...textOutputItem, phase: textPhase },\n },\n isBeta,\n );\n\n // conversation.item.done (text message)\n sendEvent(\n ws,\n {\n type: \"conversation.item.done\",\n item: {\n id: textItemId,\n object: \"realtime.item\",\n type: \"message\",\n role: \"assistant\",\n status: \"completed\",\n content: textOutputItem.content,\n },\n },\n isBeta,\n );\n\n if (ws.isClosed) {\n interruption?.cleanup();\n return;\n }\n\n allOutputItems.push(textOutputItem);\n\n // ── Tool call parts ────────────────────────────────────────────\n for (let tcIdx = 0; tcIdx < response.toolCalls.length; tcIdx++) {\n const tc = response.toolCalls[tcIdx];\n const callId = tc.id ?? generateToolCallId();\n const itemId = realtimeId(\"item\");\n const outputIndex = tcIdx + 1; // offset by 1 for the text item\n\n const toolOutputItem = {\n id: itemId,\n type: \"function_call\",\n status: \"completed\",\n call_id: callId,\n name: tc.name,\n arguments: tc.arguments,\n };\n\n // response.output_item.added\n sendEvent(\n ws,\n {\n type: \"response.output_item.added\",\n response_id: responseId,\n output_index: outputIndex,\n item: {\n id: itemId,\n type: \"function_call\",\n status: \"in_progress\",\n call_id: callId,\n name: tc.name,\n arguments: \"\",\n phase: \"final_answer\",\n },\n },\n isBeta,\n );\n\n // response.function_call_arguments.delta (chunked)\n const args = tc.arguments;\n for (let i = 0; i < args.length; i += chunkSize) {\n if (ws.isClosed) break;\n if (latency > 0) await delay(latency, interruption?.signal);\n if (interruption?.signal.aborted) {\n interrupted = true;\n break;\n }\n if (ws.isClosed) break;\n const chunk = args.slice(i, i + chunkSize);\n sendEvent(\n ws,\n {\n type: \"response.function_call_arguments.delta\",\n response_id: responseId,\n item_id: itemId,\n output_index: outputIndex,\n call_id: callId,\n delta: chunk,\n },\n isBeta,\n );\n interruption?.tick();\n if (interruption?.signal.aborted) {\n interrupted = true;\n break;\n }\n }\n\n if (interrupted) break;\n\n if (ws.isClosed) break;\n\n // response.function_call_arguments.done\n sendEvent(\n ws,\n {\n type: \"response.function_call_arguments.done\",\n response_id: responseId,\n item_id: itemId,\n output_index: outputIndex,\n call_id: callId,\n arguments: args,\n },\n isBeta,\n );\n\n if (ws.isClosed) break;\n\n // response.output_item.done\n sendEvent(\n ws,\n {\n type: \"response.output_item.done\",\n response_id: responseId,\n output_index: outputIndex,\n item: { ...toolOutputItem, phase: \"final_answer\" },\n },\n isBeta,\n );\n\n // conversation.item.done (tool call)\n sendEvent(\n ws,\n {\n type: \"conversation.item.done\",\n item: {\n id: itemId,\n object: \"realtime.item\",\n type: \"function_call\",\n status: \"completed\",\n call_id: callId,\n name: tc.name,\n arguments: args,\n },\n },\n isBeta,\n );\n\n if (ws.isClosed) break;\n\n allOutputItems.push(toolOutputItem);\n }\n\n if (interrupted) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n interruption?.cleanup();\n return;\n }\n\n interruption?.cleanup();\n\n if (ws.isClosed) return;\n\n // response.done\n sendEvent(\n ws,\n {\n type: \"response.done\",\n response: {\n id: responseId,\n object: \"realtime.response\",\n status: \"completed\",\n output: allOutputItems,\n usage: { total_tokens: 0, input_tokens: 0, output_tokens: 0 },\n },\n },\n isBeta,\n );\n\n // Accumulate into conversation for multi-turn\n conversationItems.push({\n type: \"message\",\n id: textItemId,\n role: \"assistant\",\n content: [{ type: \"text\", text: content }],\n });\n for (const item of allOutputItems.slice(1)) {\n conversationItems.push(item as RealtimeItem);\n }\n return;\n }\n\n // ── Text response ───────────────────────────────────────────────────\n if (isTextResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path: \"/v1/realtime\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n const itemId = realtimeId(\"item\");\n const contentIndex = 0;\n const outputIndex = 0;\n\n const outputItem = {\n id: itemId,\n type: \"message\",\n role: \"assistant\",\n status: \"completed\",\n content: [{ type: \"output_text\", text: response.content }],\n };\n\n // response.created\n sendEvent(\n ws,\n {\n type: \"response.created\",\n response: {\n id: responseId,\n object: \"realtime.response\",\n status: \"in_progress\",\n status_details: null,\n output: [],\n usage: null,\n },\n },\n isBeta,\n );\n\n // response.output_item.added\n sendEvent(\n ws,\n {\n type: \"response.output_item.added\",\n response_id: responseId,\n output_index: outputIndex,\n item: {\n id: itemId,\n type: \"message\",\n role: \"assistant\",\n status: \"in_progress\",\n content: [],\n phase: \"final_answer\",\n },\n },\n isBeta,\n );\n\n // response.content_part.added\n sendEvent(\n ws,\n {\n type: \"response.content_part.added\",\n response_id: responseId,\n item_id: itemId,\n output_index: outputIndex,\n content_index: contentIndex,\n part: { type: \"output_text\", text: \"\" },\n },\n isBeta,\n );\n\n // response.output_text.delta (chunked) — GA name\n const content = response.content;\n const interruption = createInterruptionSignal(fixture);\n let interrupted = false;\n\n for (let i = 0; i < content.length; i += chunkSize) {\n if (ws.isClosed) break;\n if (latency > 0) await delay(latency, interruption?.signal);\n if (interruption?.signal.aborted) {\n interrupted = true;\n break;\n }\n if (ws.isClosed) break;\n const chunk = content.slice(i, i + chunkSize);\n sendEvent(\n ws,\n {\n type: \"response.output_text.delta\",\n response_id: responseId,\n item_id: itemId,\n output_index: outputIndex,\n content_index: contentIndex,\n delta: chunk,\n },\n isBeta,\n );\n interruption?.tick();\n if (interruption?.signal.aborted) {\n interrupted = true;\n break;\n }\n }\n\n if (interrupted) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n interruption?.cleanup();\n return;\n }\n\n interruption?.cleanup();\n\n if (ws.isClosed) return;\n\n // response.output_text.done\n sendEvent(\n ws,\n {\n type: \"response.output_text.done\",\n response_id: responseId,\n item_id: itemId,\n output_index: outputIndex,\n content_index: contentIndex,\n text: content,\n },\n isBeta,\n );\n\n // response.content_part.done\n sendEvent(\n ws,\n {\n type: \"response.content_part.done\",\n response_id: responseId,\n item_id: itemId,\n output_index: outputIndex,\n content_index: contentIndex,\n part: { type: \"output_text\", text: content },\n },\n isBeta,\n );\n\n // response.output_item.done\n sendEvent(\n ws,\n {\n type: \"response.output_item.done\",\n response_id: responseId,\n output_index: outputIndex,\n item: { ...outputItem, phase: \"final_answer\" },\n },\n isBeta,\n );\n\n // conversation.item.done (text message)\n sendEvent(\n ws,\n {\n type: \"conversation.item.done\",\n item: {\n id: itemId,\n object: \"realtime.item\",\n type: \"message\",\n role: \"assistant\",\n status: \"completed\",\n content: outputItem.content,\n },\n },\n isBeta,\n );\n\n // response.done\n sendEvent(\n ws,\n {\n type: \"response.done\",\n response: {\n id: responseId,\n object: \"realtime.response\",\n status: \"completed\",\n output: [outputItem],\n usage: { total_tokens: 0, input_tokens: 0, output_tokens: 0 },\n },\n },\n isBeta,\n );\n\n // Accumulate assistant response into conversation for multi-turn\n conversationItems.push({\n type: \"message\",\n id: itemId,\n role: \"assistant\",\n content: [{ type: \"text\", text: content }],\n });\n return;\n }\n\n // ── Tool call response ──────────────────────────────────────────────\n if (isToolCallResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path: \"/v1/realtime\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n // response.created\n sendEvent(\n ws,\n {\n type: \"response.created\",\n response: {\n id: responseId,\n object: \"realtime.response\",\n status: \"in_progress\",\n status_details: null,\n output: [],\n usage: null,\n },\n },\n isBeta,\n );\n\n const outputItems: unknown[] = [];\n const interruption = createInterruptionSignal(fixture);\n let interrupted = false;\n\n for (let tcIdx = 0; tcIdx < response.toolCalls.length; tcIdx++) {\n const tc = response.toolCalls[tcIdx];\n const callId = tc.id ?? generateToolCallId();\n const itemId = realtimeId(\"item\");\n\n const outputItem = {\n id: itemId,\n type: \"function_call\",\n status: \"completed\",\n call_id: callId,\n name: tc.name,\n arguments: tc.arguments,\n };\n\n // response.output_item.added\n sendEvent(\n ws,\n {\n type: \"response.output_item.added\",\n response_id: responseId,\n output_index: tcIdx,\n item: {\n id: itemId,\n type: \"function_call\",\n status: \"in_progress\",\n call_id: callId,\n name: tc.name,\n arguments: \"\",\n phase: \"final_answer\",\n },\n },\n isBeta,\n );\n\n // response.function_call_arguments.delta (chunked)\n const args = tc.arguments;\n for (let i = 0; i < args.length; i += chunkSize) {\n if (ws.isClosed) break;\n if (latency > 0) await delay(latency, interruption?.signal);\n if (interruption?.signal.aborted) {\n interrupted = true;\n break;\n }\n if (ws.isClosed) break;\n const chunk = args.slice(i, i + chunkSize);\n sendEvent(\n ws,\n {\n type: \"response.function_call_arguments.delta\",\n response_id: responseId,\n item_id: itemId,\n output_index: tcIdx,\n call_id: callId,\n delta: chunk,\n },\n isBeta,\n );\n interruption?.tick();\n if (interruption?.signal.aborted) {\n interrupted = true;\n break;\n }\n }\n\n if (interrupted) break;\n\n if (ws.isClosed) break;\n\n // response.function_call_arguments.done\n sendEvent(\n ws,\n {\n type: \"response.function_call_arguments.done\",\n response_id: responseId,\n item_id: itemId,\n output_index: tcIdx,\n call_id: callId,\n arguments: args,\n },\n isBeta,\n );\n\n // response.output_item.done\n sendEvent(\n ws,\n {\n type: \"response.output_item.done\",\n response_id: responseId,\n output_index: tcIdx,\n item: { ...outputItem, phase: \"final_answer\" },\n },\n isBeta,\n );\n\n // conversation.item.done (tool call)\n sendEvent(\n ws,\n {\n type: \"conversation.item.done\",\n item: {\n id: itemId,\n object: \"realtime.item\",\n type: \"function_call\",\n status: \"completed\",\n call_id: callId,\n name: tc.name,\n arguments: args,\n },\n },\n isBeta,\n );\n\n outputItems.push(outputItem);\n }\n\n if (interrupted) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n interruption?.cleanup();\n return;\n }\n\n interruption?.cleanup();\n\n if (ws.isClosed) return;\n\n // response.done\n sendEvent(\n ws,\n {\n type: \"response.done\",\n response: {\n id: responseId,\n object: \"realtime.response\",\n status: \"completed\",\n output: outputItems,\n usage: { total_tokens: 0, input_tokens: 0, output_tokens: 0 },\n },\n },\n isBeta,\n );\n\n // Accumulate assistant tool calls into conversation for multi-turn\n // Reuse outputItems (which already have the correct call_id) to avoid generating divergent IDs\n for (const item of outputItems) {\n conversationItems.push(item as RealtimeItem);\n }\n return;\n }\n\n // Unknown response type\n journal.add({\n method: \"WS\",\n path: \"/v1/realtime\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 500, fixture },\n });\n buildErrorRealtimeEvent(\n ws,\n \"Fixture response did not match any known type\",\n isBeta,\n \"server_error\",\n );\n}\n"],"mappings":"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"}
@@ -1,5 +1,6 @@
1
+ const require_constants = require('./constants.cjs');
1
2
  const require_helpers = require('./helpers.cjs');
2
- const require_journal = require('./journal.cjs');
3
+ require('./journal.cjs');
3
4
  const require_router = require('./router.cjs');
4
5
  const require_sse_writer = require('./sse-writer.cjs');
5
6
  const require_interruption = require('./interruption.cjs');
@@ -28,7 +29,9 @@ function handleWebSocketResponses(ws, fixtures, journal, defaults) {
28
29
  logger.error(`WebSocket responses error: ${msg}`);
29
30
  try {
30
31
  ws.send(JSON.stringify(buildErrorEvent(msg, "server_error")));
31
- } catch {}
32
+ } catch (sendErr) {
33
+ defaults.logger.debug(`Failed to send error to client: ${sendErr instanceof Error ? sendErr.message : "unknown"}`);
34
+ }
32
35
  }));
33
36
  });
34
37
  }
@@ -55,19 +58,31 @@ async function processMessage(raw, ws, fixtures, journal, defaults) {
55
58
  temperature: parsed.temperature,
56
59
  max_output_tokens: parsed.max_output_tokens
57
60
  });
58
- const testId = defaults.testId ?? require_journal.DEFAULT_TEST_ID;
61
+ completionReq._endpointType = "chat";
62
+ const testId = defaults.testId ?? require_constants.DEFAULT_TEST_ID;
59
63
  const fixture = require_router.matchFixture(fixtures, completionReq, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform);
60
64
  if (fixture) journal.incrementFixtureMatchCount(fixture, fixtures, testId);
61
65
  if (!fixture) {
62
66
  if (require_helpers.resolveStrictMode(defaults.strict, defaults.upgradeHeaders)) {
63
67
  defaults.logger.warn(`STRICT: No fixture matched for WebSocket message`);
68
+ journal.add({
69
+ method: "WS",
70
+ path: "/v1/responses",
71
+ headers: require_helpers.flattenHeaders(defaults.upgradeHeaders ?? {}),
72
+ body: completionReq,
73
+ response: {
74
+ status: 503,
75
+ fixture: null,
76
+ ...require_helpers.strictOverrideField(defaults.strict, defaults.upgradeHeaders)
77
+ }
78
+ });
64
79
  ws.close(1008, "Strict mode: no fixture matched");
65
80
  return;
66
81
  }
67
82
  journal.add({
68
83
  method: "WS",
69
84
  path: "/v1/responses",
70
- headers: {},
85
+ headers: require_helpers.flattenHeaders(defaults.upgradeHeaders ?? {}),
71
86
  body: completionReq,
72
87
  response: {
73
88
  status: 404,
@@ -86,7 +101,7 @@ async function processMessage(raw, ws, fixtures, journal, defaults) {
86
101
  journal.add({
87
102
  method: "WS",
88
103
  path: "/v1/responses",
89
- headers: {},
104
+ headers: require_helpers.flattenHeaders(defaults.upgradeHeaders ?? {}),
90
105
  body: completionReq,
91
106
  response: {
92
107
  status,
@@ -96,18 +111,39 @@ async function processMessage(raw, ws, fixtures, journal, defaults) {
96
111
  ws.send(JSON.stringify(buildErrorEvent(response.error.message, response.error.type, response.error.code)));
97
112
  return;
98
113
  }
114
+ if (require_helpers.isContentWithToolCallsResponse(response)) {
115
+ const journalEntry = journal.add({
116
+ method: "WS",
117
+ path: "/v1/responses",
118
+ headers: require_helpers.flattenHeaders(defaults.upgradeHeaders ?? {}),
119
+ body: completionReq,
120
+ response: {
121
+ status: 200,
122
+ fixture
123
+ }
124
+ });
125
+ const events = require_responses.buildContentWithToolCallsStreamEvents(response.content, response.toolCalls, completionReq.model, chunkSize, response.reasoning, response.webSearches, require_helpers.extractOverrides(response));
126
+ const interruption = require_interruption.createInterruptionSignal(fixture);
127
+ if (!await sendEvents(ws, events, latency, interruption?.signal, interruption?.tick)) {
128
+ ws.destroy();
129
+ journalEntry.response.interrupted = true;
130
+ journalEntry.response.interruptReason = interruption?.reason();
131
+ }
132
+ interruption?.cleanup();
133
+ return;
134
+ }
99
135
  if (require_helpers.isTextResponse(response)) {
100
136
  const journalEntry = journal.add({
101
137
  method: "WS",
102
138
  path: "/v1/responses",
103
- headers: {},
139
+ headers: require_helpers.flattenHeaders(defaults.upgradeHeaders ?? {}),
104
140
  body: completionReq,
105
141
  response: {
106
142
  status: 200,
107
143
  fixture
108
144
  }
109
145
  });
110
- const events = require_responses.buildTextStreamEvents(response.content, completionReq.model, chunkSize, response.reasoning, response.webSearches);
146
+ const events = require_responses.buildTextStreamEvents(response.content, completionReq.model, chunkSize, response.reasoning, response.webSearches, require_helpers.extractOverrides(response));
111
147
  const interruption = require_interruption.createInterruptionSignal(fixture);
112
148
  if (!await sendEvents(ws, events, latency, interruption?.signal, interruption?.tick)) {
113
149
  ws.destroy();
@@ -121,14 +157,14 @@ async function processMessage(raw, ws, fixtures, journal, defaults) {
121
157
  const journalEntry = journal.add({
122
158
  method: "WS",
123
159
  path: "/v1/responses",
124
- headers: {},
160
+ headers: require_helpers.flattenHeaders(defaults.upgradeHeaders ?? {}),
125
161
  body: completionReq,
126
162
  response: {
127
163
  status: 200,
128
164
  fixture
129
165
  }
130
166
  });
131
- const events = require_responses.buildToolCallStreamEvents(response.toolCalls, completionReq.model, chunkSize);
167
+ const events = require_responses.buildToolCallStreamEvents(response.toolCalls, completionReq.model, chunkSize, response.webSearches, require_helpers.extractOverrides(response));
132
168
  const interruption = require_interruption.createInterruptionSignal(fixture);
133
169
  if (!await sendEvents(ws, events, latency, interruption?.signal, interruption?.tick)) {
134
170
  ws.destroy();
@@ -141,7 +177,7 @@ async function processMessage(raw, ws, fixtures, journal, defaults) {
141
177
  journal.add({
142
178
  method: "WS",
143
179
  path: "/v1/responses",
144
- headers: {},
180
+ headers: require_helpers.flattenHeaders(defaults.upgradeHeaders ?? {}),
145
181
  body: completionReq,
146
182
  response: {
147
183
  status: 500,
@@ -152,10 +188,10 @@ async function processMessage(raw, ws, fixtures, journal, defaults) {
152
188
  }
153
189
  async function sendEvents(ws, events, latency, signal, onChunkSent) {
154
190
  for (const event of events) {
155
- if (ws.isClosed) return true;
191
+ if (ws.isClosed) return false;
156
192
  if (latency > 0) await require_sse_writer.delay(latency, signal);
157
193
  if (signal?.aborted) return false;
158
- if (ws.isClosed) return true;
194
+ if (ws.isClosed) return false;
159
195
  ws.send(JSON.stringify(event));
160
196
  onChunkSent?.();
161
197
  if (signal?.aborted) return false;
@@ -1 +1 @@
1
- {"version":3,"file":"ws-responses.cjs","names":["responsesToCompletionRequest","DEFAULT_TEST_ID","matchFixture","resolveStrictMode","strictOverrideField","resolveResponse","isErrorResponse","isTextResponse","buildTextStreamEvents","createInterruptionSignal","isToolCallResponse","buildToolCallStreamEvents","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 { matchFixture } from \"./router.js\";\nimport {\n responsesToCompletionRequest,\n buildTextStreamEvents,\n buildToolCallStreamEvents,\n type ResponsesSSEEvent,\n} from \"./responses.js\";\nimport {\n isTextResponse,\n isToolCallResponse,\n isErrorResponse,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n} from \"./helpers.js\";\nimport { createInterruptionSignal } from \"./interruption.js\";\nimport { delay } 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 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 {\n // Connection already gone — original error already logged above\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 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 const testId = defaults.testId ?? DEFAULT_TEST_ID;\n const fixture = matchFixture(\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 defaults.logger.warn(`STRICT: No fixture matched for WebSocket message`);\n ws.close(1008, \"Strict mode: no fixture matched\");\n return;\n }\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: {},\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 // Error response\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: {},\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 // Text response\n if (isTextResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: {},\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n const events = buildTextStreamEvents(\n response.content,\n completionReq.model,\n chunkSize,\n response.reasoning,\n response.webSearches,\n );\n const interruption = createInterruptionSignal(fixture);\n const completed = await sendEvents(\n ws,\n events,\n latency,\n interruption?.signal,\n interruption?.tick,\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: {},\n body: completionReq,\n response: { status: 200, fixture },\n });\n const events = buildToolCallStreamEvents(response.toolCalls, completionReq.model, chunkSize);\n const interruption = createInterruptionSignal(fixture);\n const completed = await sendEvents(\n ws,\n events,\n latency,\n interruption?.signal,\n interruption?.tick,\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: {},\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): Promise<boolean> {\n for (const event of events) {\n if (ws.isClosed) return true;\n if (latency > 0) await delay(latency, signal);\n if (signal?.aborted) return false;\n if (ws.isClosed) return true;\n ws.send(JSON.stringify(event));\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":["responsesToCompletionRequest","DEFAULT_TEST_ID","matchFixture","resolveStrictMode","flattenHeaders","strictOverrideField","resolveResponse","isErrorResponse","isContentWithToolCallsResponse","buildContentWithToolCallsStreamEvents","extractOverrides","createInterruptionSignal","isTextResponse","buildTextStreamEvents","isToolCallResponse","buildToolCallStreamEvents","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 { matchFixture } 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 strictOverrideField,\n flattenHeaders,\n} from \"./helpers.js\";\nimport { createInterruptionSignal } from \"./interruption.js\";\nimport { delay } 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 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 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 testId = defaults.testId ?? DEFAULT_TEST_ID;\n const fixture = matchFixture(\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 defaults.logger.warn(`STRICT: No fixture matched for WebSocket message`);\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, \"Strict mode: no fixture matched\");\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 // 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 response.reasoning,\n response.webSearches,\n extractOverrides(response),\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 );\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 response.reasoning,\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 );\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 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 );\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): Promise<boolean> {\n for (const event of events) {\n if (ws.isClosed) return false;\n if (latency > 0) await delay(latency, signal);\n if (signal?.aborted) return false;\n if (ws.isClosed) return false;\n ws.send(JSON.stringify(event));\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":";;;;;;;;AAiEW,iBAHK,wBAAA,CAGL,EAAA,EAFL,mBAEK,EAAA,QAAA,EADC,OACD,EAAA,EAAA,OAAA,EAAA,OAAA,EAAA,QAAA,EAAA;SAKC,EAAA,MAAA;WAEiB,EAAA,MAAA;OAA0B,EAAA,MAAA;QAAqB,EAFhE,MAI6B;EAAmB,MAAA,CAAA,EAAA,OAAA;2BAF/B,0BAA0B;;mBAAqB,UAAA,CAEnC"}
1
+ {"version":3,"file":"ws-responses.d.cts","names":[],"sources":["../src/ws-responses.ts"],"sourcesContent":[],"mappings":";;;;;;;;AAqEW,iBAHK,wBAAA,CAGL,EAAA,EAFL,mBAEK,EAAA,QAAA,EADC,OACD,EAAA,EAAA,OAAA,EAAA,OAAA,EAAA,QAAA,EAAA;SAKC,EAAA,MAAA;WAEiB,EAAA,MAAA;OAA0B,EAAA,MAAA;QAAqB,EAFhE,MAI6B;EAAmB,MAAA,CAAA,EAAA,OAAA;2BAF/B,0BAA0B;;mBAAqB,UAAA,CAEnC"}
@@ -1 +1 @@
1
- {"version":3,"file":"ws-responses.d.ts","names":[],"sources":["../src/ws-responses.ts"],"sourcesContent":[],"mappings":";;;;;;;;AAiEW,iBAHK,wBAAA,CAGL,EAAA,EAFL,mBAEK,EAAA,QAAA,EADC,OACD,EAAA,EAAA,OAAA,EAAA,OAAA,EAAA,QAAA,EAAA;SAKC,EAAA,MAAA;WAEiB,EAAA,MAAA;OAA0B,EAAA,MAAA;QAAqB,EAFhE,MAI6B;EAAmB,MAAA,CAAA,EAAA,OAAA;2BAF/B,0BAA0B;;mBAAqB,UAAA,CAEnC"}
1
+ {"version":3,"file":"ws-responses.d.ts","names":[],"sources":["../src/ws-responses.ts"],"sourcesContent":[],"mappings":";;;;;;;;AAqEW,iBAHK,wBAAA,CAGL,EAAA,EAFL,mBAEK,EAAA,QAAA,EADC,OACD,EAAA,EAAA,OAAA,EAAA,OAAA,EAAA,QAAA,EAAA;SAKC,EAAA,MAAA;WAEiB,EAAA,MAAA;OAA0B,EAAA,MAAA;QAAqB,EAFhE,MAI6B;EAAmB,MAAA,CAAA,EAAA,OAAA;2BAF/B,0BAA0B;;mBAAqB,UAAA,CAEnC"}
@@ -1,9 +1,10 @@
1
- import { isErrorResponse, isTextResponse, isToolCallResponse, resolveResponse, resolveStrictMode, strictOverrideField } from "./helpers.js";
2
- import { DEFAULT_TEST_ID } from "./journal.js";
1
+ import { DEFAULT_TEST_ID } from "./constants.js";
2
+ import { extractOverrides, flattenHeaders, isContentWithToolCallsResponse, isErrorResponse, isTextResponse, isToolCallResponse, resolveResponse, resolveStrictMode, strictOverrideField } from "./helpers.js";
3
+ import "./journal.js";
3
4
  import { matchFixture } from "./router.js";
4
5
  import { delay } from "./sse-writer.js";
5
6
  import { createInterruptionSignal } from "./interruption.js";
6
- import { buildTextStreamEvents, buildToolCallStreamEvents, responsesToCompletionRequest } from "./responses.js";
7
+ import { buildContentWithToolCallsStreamEvents, buildTextStreamEvents, buildToolCallStreamEvents, responsesToCompletionRequest } from "./responses.js";
7
8
 
8
9
  //#region src/ws-responses.ts
9
10
  function isResponseCreateMessage(msg) {
@@ -28,7 +29,9 @@ function handleWebSocketResponses(ws, fixtures, journal, defaults) {
28
29
  logger.error(`WebSocket responses error: ${msg}`);
29
30
  try {
30
31
  ws.send(JSON.stringify(buildErrorEvent(msg, "server_error")));
31
- } catch {}
32
+ } catch (sendErr) {
33
+ defaults.logger.debug(`Failed to send error to client: ${sendErr instanceof Error ? sendErr.message : "unknown"}`);
34
+ }
32
35
  }));
33
36
  });
34
37
  }
@@ -55,19 +58,31 @@ async function processMessage(raw, ws, fixtures, journal, defaults) {
55
58
  temperature: parsed.temperature,
56
59
  max_output_tokens: parsed.max_output_tokens
57
60
  });
61
+ completionReq._endpointType = "chat";
58
62
  const testId = defaults.testId ?? DEFAULT_TEST_ID;
59
63
  const fixture = matchFixture(fixtures, completionReq, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform);
60
64
  if (fixture) journal.incrementFixtureMatchCount(fixture, fixtures, testId);
61
65
  if (!fixture) {
62
66
  if (resolveStrictMode(defaults.strict, defaults.upgradeHeaders)) {
63
67
  defaults.logger.warn(`STRICT: No fixture matched for WebSocket message`);
68
+ journal.add({
69
+ method: "WS",
70
+ path: "/v1/responses",
71
+ headers: flattenHeaders(defaults.upgradeHeaders ?? {}),
72
+ body: completionReq,
73
+ response: {
74
+ status: 503,
75
+ fixture: null,
76
+ ...strictOverrideField(defaults.strict, defaults.upgradeHeaders)
77
+ }
78
+ });
64
79
  ws.close(1008, "Strict mode: no fixture matched");
65
80
  return;
66
81
  }
67
82
  journal.add({
68
83
  method: "WS",
69
84
  path: "/v1/responses",
70
- headers: {},
85
+ headers: flattenHeaders(defaults.upgradeHeaders ?? {}),
71
86
  body: completionReq,
72
87
  response: {
73
88
  status: 404,
@@ -86,7 +101,7 @@ async function processMessage(raw, ws, fixtures, journal, defaults) {
86
101
  journal.add({
87
102
  method: "WS",
88
103
  path: "/v1/responses",
89
- headers: {},
104
+ headers: flattenHeaders(defaults.upgradeHeaders ?? {}),
90
105
  body: completionReq,
91
106
  response: {
92
107
  status,
@@ -96,18 +111,39 @@ async function processMessage(raw, ws, fixtures, journal, defaults) {
96
111
  ws.send(JSON.stringify(buildErrorEvent(response.error.message, response.error.type, response.error.code)));
97
112
  return;
98
113
  }
114
+ if (isContentWithToolCallsResponse(response)) {
115
+ const journalEntry = journal.add({
116
+ method: "WS",
117
+ path: "/v1/responses",
118
+ headers: flattenHeaders(defaults.upgradeHeaders ?? {}),
119
+ body: completionReq,
120
+ response: {
121
+ status: 200,
122
+ fixture
123
+ }
124
+ });
125
+ const events = buildContentWithToolCallsStreamEvents(response.content, response.toolCalls, completionReq.model, chunkSize, response.reasoning, response.webSearches, extractOverrides(response));
126
+ const interruption = createInterruptionSignal(fixture);
127
+ if (!await sendEvents(ws, events, latency, interruption?.signal, interruption?.tick)) {
128
+ ws.destroy();
129
+ journalEntry.response.interrupted = true;
130
+ journalEntry.response.interruptReason = interruption?.reason();
131
+ }
132
+ interruption?.cleanup();
133
+ return;
134
+ }
99
135
  if (isTextResponse(response)) {
100
136
  const journalEntry = journal.add({
101
137
  method: "WS",
102
138
  path: "/v1/responses",
103
- headers: {},
139
+ headers: flattenHeaders(defaults.upgradeHeaders ?? {}),
104
140
  body: completionReq,
105
141
  response: {
106
142
  status: 200,
107
143
  fixture
108
144
  }
109
145
  });
110
- const events = buildTextStreamEvents(response.content, completionReq.model, chunkSize, response.reasoning, response.webSearches);
146
+ const events = buildTextStreamEvents(response.content, completionReq.model, chunkSize, response.reasoning, response.webSearches, extractOverrides(response));
111
147
  const interruption = createInterruptionSignal(fixture);
112
148
  if (!await sendEvents(ws, events, latency, interruption?.signal, interruption?.tick)) {
113
149
  ws.destroy();
@@ -121,14 +157,14 @@ async function processMessage(raw, ws, fixtures, journal, defaults) {
121
157
  const journalEntry = journal.add({
122
158
  method: "WS",
123
159
  path: "/v1/responses",
124
- headers: {},
160
+ headers: flattenHeaders(defaults.upgradeHeaders ?? {}),
125
161
  body: completionReq,
126
162
  response: {
127
163
  status: 200,
128
164
  fixture
129
165
  }
130
166
  });
131
- const events = buildToolCallStreamEvents(response.toolCalls, completionReq.model, chunkSize);
167
+ const events = buildToolCallStreamEvents(response.toolCalls, completionReq.model, chunkSize, response.webSearches, extractOverrides(response));
132
168
  const interruption = createInterruptionSignal(fixture);
133
169
  if (!await sendEvents(ws, events, latency, interruption?.signal, interruption?.tick)) {
134
170
  ws.destroy();
@@ -141,7 +177,7 @@ async function processMessage(raw, ws, fixtures, journal, defaults) {
141
177
  journal.add({
142
178
  method: "WS",
143
179
  path: "/v1/responses",
144
- headers: {},
180
+ headers: flattenHeaders(defaults.upgradeHeaders ?? {}),
145
181
  body: completionReq,
146
182
  response: {
147
183
  status: 500,
@@ -152,10 +188,10 @@ async function processMessage(raw, ws, fixtures, journal, defaults) {
152
188
  }
153
189
  async function sendEvents(ws, events, latency, signal, onChunkSent) {
154
190
  for (const event of events) {
155
- if (ws.isClosed) return true;
191
+ if (ws.isClosed) return false;
156
192
  if (latency > 0) await delay(latency, signal);
157
193
  if (signal?.aborted) return false;
158
- if (ws.isClosed) return true;
194
+ if (ws.isClosed) return false;
159
195
  ws.send(JSON.stringify(event));
160
196
  onChunkSent?.();
161
197
  if (signal?.aborted) return false;