@copilotkit/aimock 1.8.0 → 1.9.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 (111) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +2 -0
  4. package/dist/a2a-types.d.ts.map +1 -1
  5. package/dist/bedrock-converse.cjs +6 -4
  6. package/dist/bedrock-converse.cjs.map +1 -1
  7. package/dist/bedrock-converse.d.cts.map +1 -1
  8. package/dist/bedrock-converse.d.ts.map +1 -1
  9. package/dist/bedrock-converse.js +7 -5
  10. package/dist/bedrock-converse.js.map +1 -1
  11. package/dist/bedrock.cjs +6 -4
  12. package/dist/bedrock.cjs.map +1 -1
  13. package/dist/bedrock.d.cts.map +1 -1
  14. package/dist/bedrock.d.ts.map +1 -1
  15. package/dist/bedrock.js +7 -5
  16. package/dist/bedrock.js.map +1 -1
  17. package/dist/cohere.cjs +3 -2
  18. package/dist/cohere.cjs.map +1 -1
  19. package/dist/cohere.d.cts.map +1 -1
  20. package/dist/cohere.d.ts.map +1 -1
  21. package/dist/cohere.js +4 -3
  22. package/dist/cohere.js.map +1 -1
  23. package/dist/embeddings.cjs +3 -2
  24. package/dist/embeddings.cjs.map +1 -1
  25. package/dist/embeddings.d.cts.map +1 -1
  26. package/dist/embeddings.d.ts.map +1 -1
  27. package/dist/embeddings.js +4 -3
  28. package/dist/embeddings.js.map +1 -1
  29. package/dist/gemini.cjs +105 -30
  30. package/dist/gemini.cjs.map +1 -1
  31. package/dist/gemini.d.cts.map +1 -1
  32. package/dist/gemini.d.ts.map +1 -1
  33. package/dist/gemini.js +106 -31
  34. package/dist/gemini.js.map +1 -1
  35. package/dist/helpers.cjs +150 -14
  36. package/dist/helpers.cjs.map +1 -1
  37. package/dist/helpers.d.cts.map +1 -1
  38. package/dist/helpers.d.ts.map +1 -1
  39. package/dist/helpers.js +147 -15
  40. package/dist/helpers.js.map +1 -1
  41. package/dist/index.cjs +1 -0
  42. package/dist/index.d.cts +2 -2
  43. package/dist/index.d.ts +2 -2
  44. package/dist/index.js +2 -2
  45. package/dist/journal.cjs +26 -9
  46. package/dist/journal.cjs.map +1 -1
  47. package/dist/journal.d.cts +10 -5
  48. package/dist/journal.d.cts.map +1 -1
  49. package/dist/journal.d.ts +10 -5
  50. package/dist/journal.d.ts.map +1 -1
  51. package/dist/journal.js +26 -10
  52. package/dist/journal.js.map +1 -1
  53. package/dist/llmock.cjs +2 -2
  54. package/dist/llmock.cjs.map +1 -1
  55. package/dist/llmock.d.cts +1 -1
  56. package/dist/llmock.d.ts +1 -1
  57. package/dist/llmock.js +2 -2
  58. package/dist/llmock.js.map +1 -1
  59. package/dist/messages.cjs +192 -2
  60. package/dist/messages.cjs.map +1 -1
  61. package/dist/messages.d.cts.map +1 -1
  62. package/dist/messages.d.ts.map +1 -1
  63. package/dist/messages.js +193 -3
  64. package/dist/messages.js.map +1 -1
  65. package/dist/ollama.cjs +6 -4
  66. package/dist/ollama.cjs.map +1 -1
  67. package/dist/ollama.d.cts.map +1 -1
  68. package/dist/ollama.d.ts.map +1 -1
  69. package/dist/ollama.js +7 -5
  70. package/dist/ollama.js.map +1 -1
  71. package/dist/responses.cjs +250 -126
  72. package/dist/responses.cjs.map +1 -1
  73. package/dist/responses.d.cts.map +1 -1
  74. package/dist/responses.d.ts.map +1 -1
  75. package/dist/responses.js +251 -127
  76. package/dist/responses.js.map +1 -1
  77. package/dist/server.cjs +42 -5
  78. package/dist/server.cjs.map +1 -1
  79. package/dist/server.d.cts.map +1 -1
  80. package/dist/server.d.ts.map +1 -1
  81. package/dist/server.js +43 -6
  82. package/dist/server.js.map +1 -1
  83. package/dist/stream-collapse.cjs +48 -40
  84. package/dist/stream-collapse.cjs.map +1 -1
  85. package/dist/stream-collapse.d.cts.map +1 -1
  86. package/dist/stream-collapse.d.ts.map +1 -1
  87. package/dist/stream-collapse.js +48 -40
  88. package/dist/stream-collapse.js.map +1 -1
  89. package/dist/types.d.cts +9 -1
  90. package/dist/types.d.cts.map +1 -1
  91. package/dist/types.d.ts +9 -1
  92. package/dist/types.d.ts.map +1 -1
  93. package/dist/ws-gemini-live.cjs +4 -2
  94. package/dist/ws-gemini-live.cjs.map +1 -1
  95. package/dist/ws-gemini-live.d.cts +1 -0
  96. package/dist/ws-gemini-live.d.ts +1 -0
  97. package/dist/ws-gemini-live.js +4 -2
  98. package/dist/ws-gemini-live.js.map +1 -1
  99. package/dist/ws-realtime.cjs +4 -2
  100. package/dist/ws-realtime.cjs.map +1 -1
  101. package/dist/ws-realtime.d.cts +1 -0
  102. package/dist/ws-realtime.d.ts +1 -0
  103. package/dist/ws-realtime.js +4 -2
  104. package/dist/ws-realtime.js.map +1 -1
  105. package/dist/ws-responses.cjs +4 -2
  106. package/dist/ws-responses.cjs.map +1 -1
  107. package/dist/ws-responses.d.cts +1 -0
  108. package/dist/ws-responses.d.ts +1 -0
  109. package/dist/ws-responses.js +4 -2
  110. package/dist/ws-responses.js.map +1 -1
  111. package/package.json +1 -1
@@ -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 type { ChatCompletionRequest, ChatMessage, Fixture } from \"./types.js\";\nimport { matchFixture } from \"./router.js\";\nimport {\n generateId,\n generateToolCallId,\n isTextResponse,\n isToolCallResponse,\n isErrorResponse,\n} from \"./helpers.js\";\nimport { createInterruptionSignal } from \"./interruption.js\";\nimport { delay } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\nimport type { WebSocketConnection } from \"./ws-framing.js\";\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: generateId(\"evt\"), ...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 },\n): void {\n const { logger } = defaults;\n const sessionId = generateId(\"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(evt(\"session.created\", { session: { id: sessionId, ...session } }));\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 },\n session: SessionConfig,\n conversationItems: RealtimeItem[],\n): Promise<void> {\n let parsed: RealtimeMessage;\n try {\n parsed = JSON.parse(raw) as RealtimeMessage;\n } catch {\n ws.send(buildErrorRealtimeEvent(\"Malformed JSON\", \"invalid_request_error\", \"invalid_json\"));\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(evt(\"session.updated\", { session: { ...session } }));\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 = generateId(\"item\");\n }\n conversationItems.push(item);\n ws.send(evt(\"conversation.item.created\", { 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 },\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 fixture = matchFixture(\n fixtures,\n completionReq,\n journal.fixtureMatchCounts,\n defaults.requestTransform,\n );\n const responseId = generateId(\"resp\");\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures);\n }\n\n if (!fixture) {\n if (defaults.strict) {\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: { status: 404, fixture: null },\n });\n // Send response.created with failed status then response.done with error\n ws.send(\n evt(\"response.created\", {\n response: { id: responseId, status: \"failed\", output: [] },\n }),\n );\n ws.send(\n evt(\"response.done\", {\n response: {\n id: responseId,\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 },\n }),\n );\n return;\n }\n\n const response = fixture.response;\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: { id: responseId, status: \"failed\", output: [] },\n }),\n );\n ws.send(\n evt(\"response.done\", {\n response: {\n id: responseId,\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 },\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 = generateId(\"item\");\n const contentIndex = 0;\n const outputIndex = 0;\n\n const outputItem = {\n id: itemId,\n type: \"message\",\n role: \"assistant\",\n content: [{ type: \"text\", text: response.content }],\n };\n\n // response.created\n ws.send(\n evt(\"response.created\", {\n response: { id: responseId, status: \"in_progress\", output: [] },\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: { id: itemId, type: \"message\", role: \"assistant\", content: [] },\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: { id: responseId, status: \"completed\", output: [outputItem] },\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: { id: responseId, status: \"in_progress\", output: [] },\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 = generateId(\"item\");\n\n const outputItem = {\n id: itemId,\n type: \"function_call\",\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 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: { id: responseId, status: \"completed\", output: outputItems },\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 type { ChatCompletionRequest, ChatMessage, Fixture } from \"./types.js\";\nimport { matchFixture } from \"./router.js\";\nimport {\n generateId,\n generateToolCallId,\n isTextResponse,\n isToolCallResponse,\n isErrorResponse,\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// ─── 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: generateId(\"evt\"), ...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 },\n): void {\n const { logger } = defaults;\n const sessionId = generateId(\"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(evt(\"session.created\", { session: { id: sessionId, ...session } }));\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 },\n session: SessionConfig,\n conversationItems: RealtimeItem[],\n): Promise<void> {\n let parsed: RealtimeMessage;\n try {\n parsed = JSON.parse(raw) as RealtimeMessage;\n } catch {\n ws.send(buildErrorRealtimeEvent(\"Malformed JSON\", \"invalid_request_error\", \"invalid_json\"));\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(evt(\"session.updated\", { session: { ...session } }));\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 = generateId(\"item\");\n }\n conversationItems.push(item);\n ws.send(evt(\"conversation.item.created\", { 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 },\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 = generateId(\"resp\");\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n }\n\n if (!fixture) {\n if (defaults.strict) {\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: { status: 404, fixture: null },\n });\n // Send response.created with failed status then response.done with error\n ws.send(\n evt(\"response.created\", {\n response: { id: responseId, status: \"failed\", output: [] },\n }),\n );\n ws.send(\n evt(\"response.done\", {\n response: {\n id: responseId,\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 },\n }),\n );\n return;\n }\n\n const response = fixture.response;\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: { id: responseId, status: \"failed\", output: [] },\n }),\n );\n ws.send(\n evt(\"response.done\", {\n response: {\n id: responseId,\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 },\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 = generateId(\"item\");\n const contentIndex = 0;\n const outputIndex = 0;\n\n const outputItem = {\n id: itemId,\n type: \"message\",\n role: \"assistant\",\n content: [{ type: \"text\", text: response.content }],\n };\n\n // response.created\n ws.send(\n evt(\"response.created\", {\n response: { id: responseId, status: \"in_progress\", output: [] },\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: { id: itemId, type: \"message\", role: \"assistant\", content: [] },\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: { id: responseId, status: \"completed\", output: [outputItem] },\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: { id: responseId, status: \"in_progress\", output: [] },\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 = generateId(\"item\");\n\n const outputItem = {\n id: itemId,\n type: \"function_call\",\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 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: { id: responseId, status: \"completed\", output: outputItems },\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,4 +1,5 @@
1
1
  const require_helpers = require('./helpers.cjs');
2
+ const require_journal = require('./journal.cjs');
2
3
  const require_router = require('./router.cjs');
3
4
  const require_sse_writer = require('./sse-writer.cjs');
4
5
  const require_interruption = require('./interruption.cjs');
@@ -53,8 +54,9 @@ async function processMessage(raw, ws, fixtures, journal, defaults) {
53
54
  temperature: parsed.temperature,
54
55
  max_output_tokens: parsed.max_output_tokens
55
56
  });
56
- const fixture = require_router.matchFixture(fixtures, completionReq, journal.fixtureMatchCounts, defaults.requestTransform);
57
- if (fixture) journal.incrementFixtureMatchCount(fixture, fixtures);
57
+ const testId = defaults.testId ?? require_journal.DEFAULT_TEST_ID;
58
+ const fixture = require_router.matchFixture(fixtures, completionReq, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform);
59
+ if (fixture) journal.incrementFixtureMatchCount(fixture, fixtures, testId);
58
60
  if (!fixture) {
59
61
  if (defaults.strict) {
60
62
  defaults.logger.warn(`STRICT: No fixture matched for WebSocket message`);
@@ -1 +1 @@
1
- {"version":3,"file":"ws-responses.cjs","names":["responsesToCompletionRequest","matchFixture","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 { isTextResponse, isToolCallResponse, isErrorResponse } from \"./helpers.js\";\nimport { createInterruptionSignal } from \"./interruption.js\";\nimport { delay } from \"./sse-writer.js\";\nimport 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 },\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 },\n): Promise<void> {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n ws.send(\n JSON.stringify(buildErrorEvent(\"Malformed JSON\", \"invalid_request_error\", \"invalid_json\")),\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 fixture = matchFixture(\n fixtures,\n completionReq,\n journal.fixtureMatchCounts,\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures);\n }\n\n if (!fixture) {\n if (defaults.strict) {\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: { status: 404, fixture: null },\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 = fixture.response;\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","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 { isTextResponse, isToolCallResponse, isErrorResponse } 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 },\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 },\n): Promise<void> {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n ws.send(\n JSON.stringify(buildErrorEvent(\"Malformed JSON\", \"invalid_request_error\", \"invalid_json\")),\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 (defaults.strict) {\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: { status: 404, fixture: null },\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 = fixture.response;\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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"}
@@ -12,6 +12,7 @@ declare function handleWebSocketResponses(ws: WebSocketConnection, fixtures: Fix
12
12
  logger: Logger;
13
13
  strict?: boolean;
14
14
  requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;
15
+ testId?: string;
15
16
  }): void;
16
17
  //# sourceMappingURL=ws-responses.d.ts.map
17
18
  //#endregion
@@ -12,6 +12,7 @@ declare function handleWebSocketResponses(ws: WebSocketConnection, fixtures: Fix
12
12
  logger: Logger;
13
13
  strict?: boolean;
14
14
  requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;
15
+ testId?: string;
15
16
  }): void;
16
17
  //# sourceMappingURL=ws-responses.d.ts.map
17
18
  //#endregion
@@ -1,4 +1,5 @@
1
1
  import { isErrorResponse, isTextResponse, isToolCallResponse } from "./helpers.js";
2
+ import { DEFAULT_TEST_ID } from "./journal.js";
2
3
  import { matchFixture } from "./router.js";
3
4
  import { delay } from "./sse-writer.js";
4
5
  import { createInterruptionSignal } from "./interruption.js";
@@ -53,8 +54,9 @@ async function processMessage(raw, ws, fixtures, journal, defaults) {
53
54
  temperature: parsed.temperature,
54
55
  max_output_tokens: parsed.max_output_tokens
55
56
  });
56
- const fixture = matchFixture(fixtures, completionReq, journal.fixtureMatchCounts, defaults.requestTransform);
57
- if (fixture) journal.incrementFixtureMatchCount(fixture, fixtures);
57
+ const testId = defaults.testId ?? DEFAULT_TEST_ID;
58
+ const fixture = matchFixture(fixtures, completionReq, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform);
59
+ if (fixture) journal.incrementFixtureMatchCount(fixture, fixtures, testId);
58
60
  if (!fixture) {
59
61
  if (defaults.strict) {
60
62
  defaults.logger.warn(`STRICT: No fixture matched for WebSocket message`);
@@ -1 +1 @@
1
- {"version":3,"file":"ws-responses.js","names":[],"sources":["../src/ws-responses.ts"],"sourcesContent":["/**\n * WebSocket handler for OpenAI Responses API.\n *\n * Accepts `{ type: \"response.create\", model: \"...\", input: [...] }` messages over\n * WebSocket and sends back the same Responses API SSE events as the HTTP\n * handler, but as individual WebSocket text frames.\n */\n\nimport type { ChatCompletionRequest, Fixture } from \"./types.js\";\nimport { matchFixture } from \"./router.js\";\nimport {\n responsesToCompletionRequest,\n buildTextStreamEvents,\n buildToolCallStreamEvents,\n type ResponsesSSEEvent,\n} from \"./responses.js\";\nimport { isTextResponse, isToolCallResponse, isErrorResponse } from \"./helpers.js\";\nimport { createInterruptionSignal } from \"./interruption.js\";\nimport { delay } from \"./sse-writer.js\";\nimport 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 },\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 },\n): Promise<void> {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n ws.send(\n JSON.stringify(buildErrorEvent(\"Malformed JSON\", \"invalid_request_error\", \"invalid_json\")),\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 fixture = matchFixture(\n fixtures,\n completionReq,\n journal.fixtureMatchCounts,\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures);\n }\n\n if (!fixture) {\n if (defaults.strict) {\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: { status: 404, fixture: null },\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 = fixture.response;\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.js","names":[],"sources":["../src/ws-responses.ts"],"sourcesContent":["/**\n * WebSocket handler for OpenAI Responses API.\n *\n * Accepts `{ type: \"response.create\", model: \"...\", input: [...] }` messages over\n * WebSocket and sends back the same Responses API SSE events as the HTTP\n * handler, but as individual WebSocket text frames.\n */\n\nimport type { ChatCompletionRequest, Fixture } from \"./types.js\";\nimport { matchFixture } from \"./router.js\";\nimport {\n responsesToCompletionRequest,\n buildTextStreamEvents,\n buildToolCallStreamEvents,\n type ResponsesSSEEvent,\n} from \"./responses.js\";\nimport { isTextResponse, isToolCallResponse, isErrorResponse } 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 },\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 },\n): Promise<void> {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n ws.send(\n JSON.stringify(buildErrorEvent(\"Malformed JSON\", \"invalid_request_error\", \"invalid_json\")),\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 (defaults.strict) {\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: { status: 404, fixture: null },\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 = fixture.response;\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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@copilotkit/aimock",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "Mock infrastructure for AI application testing — LLM APIs, MCP tools, A2A agents, vector databases, search, and more. Zero dependencies.",
5
5
  "license": "MIT",
6
6
  "repository": {