@copilotkit/aimock 1.19.3 → 1.19.5

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 (88) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +35 -0
  3. package/dist/agui-types.d.cts.map +1 -1
  4. package/dist/agui-types.d.ts.map +1 -1
  5. package/dist/bedrock-converse.cjs +11 -13
  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 +11 -13
  10. package/dist/bedrock-converse.js.map +1 -1
  11. package/dist/bedrock.cjs +28 -3
  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 +28 -3
  16. package/dist/bedrock.js.map +1 -1
  17. package/dist/cohere.cjs +59 -35
  18. package/dist/cohere.cjs.map +1 -1
  19. package/dist/cohere.d.cts +14 -2
  20. package/dist/cohere.d.cts.map +1 -1
  21. package/dist/cohere.d.ts +14 -2
  22. package/dist/cohere.d.ts.map +1 -1
  23. package/dist/cohere.js +59 -35
  24. package/dist/cohere.js.map +1 -1
  25. package/dist/gemini.cjs +4 -2
  26. package/dist/gemini.cjs.map +1 -1
  27. package/dist/gemini.d.cts.map +1 -1
  28. package/dist/gemini.d.ts.map +1 -1
  29. package/dist/gemini.js +5 -3
  30. package/dist/gemini.js.map +1 -1
  31. package/dist/messages.cjs +24 -4
  32. package/dist/messages.cjs.map +1 -1
  33. package/dist/messages.d.cts.map +1 -1
  34. package/dist/messages.d.ts.map +1 -1
  35. package/dist/messages.js +24 -4
  36. package/dist/messages.js.map +1 -1
  37. package/dist/moderation.cjs +6 -2
  38. package/dist/moderation.cjs.map +1 -1
  39. package/dist/moderation.d.cts.map +1 -1
  40. package/dist/moderation.d.ts.map +1 -1
  41. package/dist/moderation.js +6 -2
  42. package/dist/moderation.js.map +1 -1
  43. package/dist/ollama.cjs +25 -8
  44. package/dist/ollama.cjs.map +1 -1
  45. package/dist/ollama.d.cts +7 -0
  46. package/dist/ollama.d.cts.map +1 -1
  47. package/dist/ollama.d.ts +7 -0
  48. package/dist/ollama.d.ts.map +1 -1
  49. package/dist/ollama.js +25 -8
  50. package/dist/ollama.js.map +1 -1
  51. package/dist/rerank.cjs +4 -10
  52. package/dist/rerank.cjs.map +1 -1
  53. package/dist/rerank.js +4 -10
  54. package/dist/rerank.js.map +1 -1
  55. package/dist/responses.cjs +3 -1
  56. package/dist/responses.cjs.map +1 -1
  57. package/dist/responses.d.cts.map +1 -1
  58. package/dist/responses.d.ts.map +1 -1
  59. package/dist/responses.js +3 -1
  60. package/dist/responses.js.map +1 -1
  61. package/dist/search.cjs +7 -1
  62. package/dist/search.cjs.map +1 -1
  63. package/dist/search.js +7 -1
  64. package/dist/search.js.map +1 -1
  65. package/dist/server.cjs +12 -2
  66. package/dist/server.cjs.map +1 -1
  67. package/dist/server.d.cts.map +1 -1
  68. package/dist/server.d.ts.map +1 -1
  69. package/dist/server.js +12 -2
  70. package/dist/server.js.map +1 -1
  71. package/dist/transcription.cjs +7 -6
  72. package/dist/transcription.cjs.map +1 -1
  73. package/dist/transcription.js +7 -6
  74. package/dist/transcription.js.map +1 -1
  75. package/dist/vector-types.d.ts.map +1 -1
  76. package/dist/ws-gemini-live.cjs +37 -29
  77. package/dist/ws-gemini-live.cjs.map +1 -1
  78. package/dist/ws-gemini-live.d.cts.map +1 -1
  79. package/dist/ws-gemini-live.d.ts.map +1 -1
  80. package/dist/ws-gemini-live.js +37 -29
  81. package/dist/ws-gemini-live.js.map +1 -1
  82. package/dist/ws-realtime.cjs +84 -15
  83. package/dist/ws-realtime.cjs.map +1 -1
  84. package/dist/ws-realtime.d.cts.map +1 -1
  85. package/dist/ws-realtime.d.ts.map +1 -1
  86. package/dist/ws-realtime.js +84 -16
  87. package/dist/ws-realtime.js.map +1 -1
  88. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"ws-gemini-live.js","names":[],"sources":["../src/ws-gemini-live.ts"],"sourcesContent":["/**\n * WebSocket handler for Gemini Live BidiGenerateContent API.\n *\n * Accepts setup, clientContent, and toolResponse messages over WebSocket\n * and responds with setupComplete, serverContent, toolCall, and error\n * messages in the Gemini Live streaming format.\n */\n\nimport type {\n Fixture,\n ChatMessage,\n ChatCompletionRequest,\n ToolDefinition,\n AudioResponse,\n} from \"./types.js\";\nimport { matchFixture } from \"./router.js\";\nimport {\n isTextResponse,\n isToolCallResponse,\n isContentWithToolCallsResponse,\n isErrorResponse,\n isAudioResponse,\n formatToMime,\n generateToolCallId,\n resolveResponse,\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// ─── Gemini Live protocol types ─────────────────────────────────────────────\n\ninterface GeminiLivePart {\n text?: string;\n thought?: boolean;\n functionCall?: { name: string; args: Record<string, unknown> };\n functionResponse?: { name: string; response: unknown; id?: string };\n inlineData?: { mimeType: string; data: string };\n}\n\ninterface GeminiLiveTurn {\n role: string;\n parts: GeminiLivePart[];\n}\n\ninterface GeminiLiveFunctionDeclaration {\n name: string;\n description?: string;\n parameters?: object;\n}\n\ninterface GeminiLiveToolDef {\n functionDeclarations?: GeminiLiveFunctionDeclaration[];\n}\n\ninterface GeminiLiveSetup {\n model?: string;\n generationConfig?: Record<string, unknown>;\n tools?: GeminiLiveToolDef[];\n}\n\ninterface GeminiLiveClientContent {\n turns: GeminiLiveTurn[];\n turnComplete?: boolean;\n}\n\ninterface GeminiLiveFunctionResponse {\n id?: string;\n name: string;\n response: unknown;\n}\n\ninterface GeminiLiveToolResponse {\n functionResponses: GeminiLiveFunctionResponse[];\n}\n\ninterface GeminiLiveMessage {\n setup?: GeminiLiveSetup;\n clientContent?: GeminiLiveClientContent;\n toolResponse?: GeminiLiveToolResponse;\n}\n\n// ─── Session state ──────────────────────────────────────────────────────────\n\ninterface SessionState {\n setupDone: boolean;\n model: string;\n tools: ToolDefinition[];\n conversationHistory: ChatMessage[];\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────────\n\nconst WS_PATH = \"/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent\";\n\n/**\n * Convert Gemini Live turns into ChatMessage[] for fixture matching.\n */\nfunction geminiTurnsToMessages(turns: GeminiLiveTurn[]): ChatMessage[] {\n const messages: ChatMessage[] = [];\n\n for (const turn of turns) {\n const role = turn.role ?? \"user\";\n\n if (role === \"user\") {\n const funcResponses = turn.parts.filter((p) => p.functionResponse);\n // inlineData parts (e.g. client audio input) are silently skipped —\n // only text and functionResponse parts are relevant for fixture matching.\n const textParts = turn.parts.filter((p) => p.text !== undefined && !p.thought);\n\n if (funcResponses.length > 0) {\n for (let i = 0; i < funcResponses.length; i++) {\n const part = funcResponses[i];\n const fr = part.functionResponse!;\n messages.push({\n role: \"tool\",\n content: typeof fr.response === \"string\" ? fr.response : JSON.stringify(fr.response),\n tool_call_id: fr.id ?? `call_gemini_${fr.name}_${i}`,\n });\n }\n if (textParts.length > 0) {\n messages.push({\n role: \"user\",\n content: textParts.map((p) => p.text!).join(\"\"),\n });\n }\n } else {\n const text = textParts.map((p) => p.text!).join(\"\");\n messages.push({ role: \"user\", content: text });\n }\n } else if (role === \"model\") {\n const funcCalls = turn.parts.filter((p) => p.functionCall);\n const textParts = turn.parts.filter((p) => p.text !== undefined && !p.thought);\n\n if (funcCalls.length > 0) {\n messages.push({\n role: \"assistant\",\n content: null,\n tool_calls: funcCalls.map((p, i) => ({\n id: `call_gemini_${p.functionCall!.name}_${i}`,\n type: \"function\" as const,\n function: {\n name: p.functionCall!.name,\n arguments: JSON.stringify(p.functionCall!.args),\n },\n })),\n });\n } else {\n const text = textParts.map((p) => p.text!).join(\"\");\n messages.push({ role: \"assistant\", content: text });\n }\n }\n }\n\n return messages;\n}\n\n/**\n * Convert toolResponse messages into ChatMessage[] for fixture matching.\n */\nfunction toolResponseToMessages(toolResponse: GeminiLiveToolResponse): ChatMessage[] {\n return toolResponse.functionResponses.map((fr, i) => ({\n role: \"tool\" as const,\n content: typeof fr.response === \"string\" ? fr.response : JSON.stringify(fr.response),\n tool_call_id: fr.id ?? `call_gemini_${fr.name}_${i}`,\n }));\n}\n\n/**\n * Convert Gemini tool definitions to ChatCompletion ToolDefinition[].\n */\nfunction convertTools(geminiTools?: GeminiLiveToolDef[]): ToolDefinition[] {\n if (!geminiTools || geminiTools.length === 0) return [];\n const decls = geminiTools.flatMap((t) => t.functionDeclarations ?? []);\n return decls.map((d) => ({\n type: \"function\" as const,\n function: {\n name: d.name,\n description: d.description,\n parameters: d.parameters,\n },\n }));\n}\n\n// ─── Main handler ───────────────────────────────────────────────────────────\n\nexport function handleWebSocketGeminiLive(\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 session: SessionState = {\n setupDone: false,\n model: defaults.model,\n tools: [],\n conversationHistory: [],\n };\n\n let pending = Promise.resolve();\n ws.on(\"message\", (raw: string) => {\n pending = pending.then(() =>\n processMessage(raw, ws, fixtures, journal, defaults, session).catch((err: unknown) => {\n const msg = err instanceof Error ? err.message : \"Internal error\";\n logger.error(`WebSocket Gemini Live error: ${msg}`);\n try {\n ws.send(\n JSON.stringify({\n error: { code: 500, message: msg, status: \"INTERNAL\" },\n }),\n );\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 session: SessionState,\n): Promise<void> {\n let parsed: GeminiLiveMessage;\n try {\n parsed = JSON.parse(raw) as GeminiLiveMessage;\n } catch {\n ws.send(\n JSON.stringify({\n error: { code: 400, message: \"Malformed JSON\", status: \"INVALID_ARGUMENT\" },\n }),\n );\n return;\n }\n\n // Handle setup message\n if (parsed.setup) {\n session.setupDone = true;\n session.model = parsed.setup.model ?? defaults.model;\n session.tools = convertTools(parsed.setup.tools);\n ws.send(JSON.stringify({ setupComplete: {} }));\n return;\n }\n\n // Reject messages before setup\n if (!session.setupDone) {\n ws.send(\n JSON.stringify({\n error: { code: 400, message: \"Setup required\", status: \"FAILED_PRECONDITION\" },\n }),\n );\n return;\n }\n\n // Build messages from this interaction\n let newMessages: ChatMessage[];\n\n if (parsed.clientContent) {\n if (!parsed.clientContent.turns || !Array.isArray(parsed.clientContent.turns)) {\n ws.send(\n JSON.stringify({\n error: {\n code: 400,\n message: \"Missing 'turns' in clientContent\",\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n newMessages = geminiTurnsToMessages(parsed.clientContent.turns);\n } else if (parsed.toolResponse) {\n if (\n !parsed.toolResponse.functionResponses ||\n !Array.isArray(parsed.toolResponse.functionResponses)\n ) {\n ws.send(\n JSON.stringify({\n error: {\n code: 400,\n message: \"Missing 'functionResponses' in toolResponse\",\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n newMessages = toolResponseToMessages(parsed.toolResponse);\n } else {\n ws.send(\n JSON.stringify({\n error: {\n code: 400,\n message: \"Expected clientContent or toolResponse\",\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n\n // Build completion request for fixture matching (include new messages speculatively)\n const completionReq: ChatCompletionRequest = {\n model: session.model,\n messages: [...session.conversationHistory, ...newMessages],\n stream: true,\n tools: session.tools.length > 0 ? session.tools : undefined,\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 path = WS_PATH;\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 journal.add({\n method: \"WS\",\n path,\n headers: {},\n body: completionReq,\n response: { status: 404, fixture: null },\n });\n ws.close(1008, \"Strict mode: no fixture matched\");\n return;\n }\n journal.add({\n method: \"WS\",\n path,\n headers: {},\n body: completionReq,\n response: { status: 404, fixture: null },\n });\n ws.send(\n JSON.stringify({\n error: { code: 404, message: \"No fixture matched\", status: \"NOT_FOUND\" },\n }),\n );\n return;\n }\n\n // Commit messages to conversation history only after successful fixture match\n session.conversationHistory.push(...newMessages);\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,\n headers: {},\n body: completionReq,\n response: { status, fixture },\n });\n ws.send(\n JSON.stringify({\n error: {\n code: status,\n message: response.error.message,\n status: response.error.type ?? \"INTERNAL\",\n },\n }),\n );\n return;\n }\n\n // Audio response — single frame with inlineData and turnComplete: true\n if (isAudioResponse(response)) {\n journal.add({\n method: \"WS\",\n path,\n headers: {},\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n const audioResp = response as AudioResponse;\n let mimeType: string;\n let data: string;\n\n if (typeof audioResp.audio === \"string\") {\n mimeType = formatToMime(audioResp.format ?? \"mp3\");\n data = audioResp.audio;\n } else {\n mimeType = audioResp.audio.contentType ?? \"audio/mpeg\";\n data = audioResp.audio.b64Json;\n }\n\n ws.send(\n JSON.stringify({\n serverContent: {\n modelTurn: {\n parts: [{ inlineData: { mimeType, data } }],\n },\n turnComplete: true,\n },\n }),\n );\n\n session.conversationHistory.push({\n role: \"assistant\",\n content: \"[audio]\",\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,\n headers: {},\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n const content = response.content;\n const chunkList: string[] = [];\n for (let i = 0; i < content.length; i += chunkSize) {\n chunkList.push(content.slice(i, i + chunkSize));\n }\n\n const interruption = createInterruptionSignal(fixture);\n let interrupted = false;\n\n // Stream text content chunks\n if (content.length === 0) {\n if (!ws.isClosed) {\n ws.send(\n JSON.stringify({\n serverContent: {\n modelTurn: { parts: [{ text: \"\" }] },\n turnComplete: false,\n },\n }),\n );\n }\n } else {\n for (let i = 0; i < chunkList.length; i++) {\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\n ws.send(\n JSON.stringify({\n serverContent: {\n modelTurn: { parts: [{ text: chunkList[i] }] },\n turnComplete: false,\n },\n }),\n );\n interruption?.tick();\n if (interruption?.signal.aborted) {\n interrupted = true;\n break;\n }\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 // Pre-compute tool calls with stable IDs so wire message and history match\n const resolvedToolCalls = response.toolCalls.map((tc) => ({\n ...tc,\n resolvedId: tc.id ?? generateToolCallId(),\n }));\n\n // Send tool calls\n if (!ws.isClosed) {\n if (latency > 0) await delay(latency, interruption?.signal);\n if (interruption?.signal.aborted) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n interruption?.cleanup();\n return;\n }\n\n const functionCalls = resolvedToolCalls.map((tc) => {\n let argsObj: Record<string, unknown>;\n try {\n argsObj = JSON.parse(tc.arguments || \"{}\") as Record<string, unknown>;\n } catch {\n defaults.logger.warn(\n `Malformed JSON in fixture tool call arguments for \"${tc.name}\": ${tc.arguments}`,\n );\n argsObj = {};\n }\n return {\n name: tc.name,\n args: argsObj,\n id: tc.resolvedId,\n };\n });\n\n ws.send(JSON.stringify({ toolCall: { functionCalls } }));\n interruption?.tick();\n }\n\n if (interruption?.signal.aborted) {\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 // Send turnComplete\n if (!ws.isClosed) {\n ws.send(\n JSON.stringify({\n serverContent: { turnComplete: true },\n }),\n );\n }\n\n // Add to conversation history using the same resolved IDs from the wire message\n session.conversationHistory.push({\n role: \"assistant\",\n content: content || null,\n tool_calls: resolvedToolCalls.map((tc) => ({\n id: tc.resolvedId,\n type: \"function\" as const,\n function: {\n name: tc.name,\n arguments: tc.arguments,\n },\n })),\n });\n return;\n }\n\n // Text response — stream chunks with serverContent\n if (isTextResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path,\n headers: {},\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n const content = response.content;\n\n if (content.length === 0) {\n if (ws.isClosed) return;\n ws.send(\n JSON.stringify({\n serverContent: {\n modelTurn: { parts: [{ text: \"\" }] },\n turnComplete: true,\n },\n }),\n );\n return;\n }\n\n // Chunk the content\n const chunks: string[] = [];\n for (let i = 0; i < content.length; i += chunkSize) {\n chunks.push(content.slice(i, i + chunkSize));\n }\n\n const interruption = createInterruptionSignal(fixture);\n let interrupted = false;\n\n for (let i = 0; i < chunks.length; i++) {\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\n const isLast = i === chunks.length - 1;\n ws.send(\n JSON.stringify({\n serverContent: {\n modelTurn: { parts: [{ text: chunks[i] }] },\n turnComplete: isLast,\n },\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 // Add assistant response to conversation history\n session.conversationHistory.push({ role: \"assistant\", content });\n return;\n }\n\n // Tool call response\n if (isToolCallResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path,\n headers: {},\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n const interruption = createInterruptionSignal(fixture);\n\n if (ws.isClosed) {\n interruption?.cleanup();\n return;\n }\n if (latency > 0) await delay(latency, interruption?.signal);\n if (interruption?.signal.aborted) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n interruption?.cleanup();\n return;\n }\n if (ws.isClosed) {\n interruption?.cleanup();\n return;\n }\n\n const functionCalls = response.toolCalls.map((tc, i) => {\n let argsObj: Record<string, unknown>;\n try {\n argsObj = JSON.parse(tc.arguments || \"{}\") as Record<string, unknown>;\n } catch {\n defaults.logger.warn(\n `Malformed JSON in fixture tool call arguments for \"${tc.name}\": ${tc.arguments}`,\n );\n argsObj = {};\n }\n return {\n name: tc.name,\n args: argsObj,\n id: tc.id ?? `call_gemini_${tc.name}_${i}`,\n };\n });\n\n ws.send(JSON.stringify({ toolCall: { functionCalls } }));\n interruption?.tick();\n\n if (interruption?.signal.aborted) {\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 // Add assistant tool_calls to conversation history\n session.conversationHistory.push({\n role: \"assistant\",\n content: null,\n tool_calls: response.toolCalls.map((tc, i) => ({\n id: tc.id ?? `call_gemini_${tc.name}_${i}`,\n type: \"function\" as const,\n function: {\n name: tc.name,\n arguments: tc.arguments,\n },\n })),\n });\n return;\n }\n\n // Unknown response type\n journal.add({\n method: \"WS\",\n path,\n headers: {},\n body: completionReq,\n response: { status: 500, fixture },\n });\n ws.send(\n JSON.stringify({\n error: {\n code: 500,\n message: \"Fixture response did not match any known type\",\n status: \"INTERNAL\",\n },\n }),\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ws-gemini-live.js","names":[],"sources":["../src/ws-gemini-live.ts"],"sourcesContent":["/**\n * WebSocket handler for Gemini Live BidiGenerateContent API.\n *\n * Accepts setup, clientContent, and toolResponse messages over WebSocket\n * and responds with setupComplete, serverContent, toolCall, and error\n * messages in the Gemini Live streaming format.\n */\n\nimport type {\n Fixture,\n ChatMessage,\n ChatCompletionRequest,\n ToolDefinition,\n AudioResponse,\n} from \"./types.js\";\nimport { matchFixture } from \"./router.js\";\nimport {\n isTextResponse,\n isToolCallResponse,\n isContentWithToolCallsResponse,\n isErrorResponse,\n isAudioResponse,\n formatToMime,\n generateToolCallId,\n resolveResponse,\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// ─── Gemini Live protocol types ─────────────────────────────────────────────\n\ninterface GeminiLivePart {\n text?: string;\n thought?: boolean;\n functionCall?: { name: string; args: Record<string, unknown> };\n functionResponse?: { name: string; response: unknown; id?: string };\n inlineData?: { mimeType: string; data: string };\n}\n\ninterface GeminiLiveTurn {\n role: string;\n parts: GeminiLivePart[];\n}\n\ninterface GeminiLiveFunctionDeclaration {\n name: string;\n description?: string;\n parameters?: object;\n}\n\ninterface GeminiLiveToolDef {\n functionDeclarations?: GeminiLiveFunctionDeclaration[];\n}\n\ninterface GeminiLiveSetup {\n model?: string;\n generationConfig?: Record<string, unknown>;\n tools?: GeminiLiveToolDef[];\n}\n\ninterface GeminiLiveClientContent {\n turns: GeminiLiveTurn[];\n turnComplete?: boolean;\n}\n\ninterface GeminiLiveFunctionResponse {\n id?: string;\n name: string;\n response: unknown;\n}\n\ninterface GeminiLiveToolResponse {\n functionResponses: GeminiLiveFunctionResponse[];\n}\n\ninterface GeminiLiveMessage {\n setup?: GeminiLiveSetup;\n config?: GeminiLiveSetup;\n clientContent?: GeminiLiveClientContent;\n toolResponse?: GeminiLiveToolResponse;\n}\n\n// ─── Session state ──────────────────────────────────────────────────────────\n\ninterface SessionState {\n setupDone: boolean;\n model: string;\n tools: ToolDefinition[];\n conversationHistory: ChatMessage[];\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────────\n\nconst WS_PATH = \"/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent\";\n\n/**\n * Map HTTP status codes to gRPC error codes.\n * Gemini Live uses gRPC codes, not HTTP status codes.\n */\nfunction httpToGrpc(httpCode: number): number {\n switch (httpCode) {\n case 400:\n return 3; // INVALID_ARGUMENT\n case 401:\n return 16; // UNAUTHENTICATED\n case 403:\n return 7; // PERMISSION_DENIED\n case 404:\n return 5; // NOT_FOUND\n case 409:\n return 10; // ABORTED\n case 429:\n return 8; // RESOURCE_EXHAUSTED\n case 501:\n return 12; // UNIMPLEMENTED\n case 503:\n return 14; // UNAVAILABLE\n default:\n return 13; // INTERNAL\n }\n}\n\n/**\n * Convert Gemini Live turns into ChatMessage[] for fixture matching.\n */\nfunction geminiTurnsToMessages(turns: GeminiLiveTurn[]): ChatMessage[] {\n const messages: ChatMessage[] = [];\n\n for (const turn of turns) {\n const role = turn.role ?? \"user\";\n\n if (role === \"user\") {\n const funcResponses = turn.parts.filter((p) => p.functionResponse);\n // inlineData parts (e.g. client audio input) are silently skipped —\n // only text and functionResponse parts are relevant for fixture matching.\n const textParts = turn.parts.filter((p) => p.text !== undefined && !p.thought);\n\n if (funcResponses.length > 0) {\n for (let i = 0; i < funcResponses.length; i++) {\n const part = funcResponses[i];\n const fr = part.functionResponse!;\n messages.push({\n role: \"tool\",\n content: typeof fr.response === \"string\" ? fr.response : JSON.stringify(fr.response),\n tool_call_id: fr.id ?? `call_gemini_${fr.name}_${i}`,\n });\n }\n if (textParts.length > 0) {\n messages.push({\n role: \"user\",\n content: textParts.map((p) => p.text!).join(\"\"),\n });\n }\n } else {\n const text = textParts.map((p) => p.text!).join(\"\");\n messages.push({ role: \"user\", content: text });\n }\n } else if (role === \"model\") {\n const funcCalls = turn.parts.filter((p) => p.functionCall);\n const textParts = turn.parts.filter((p) => p.text !== undefined && !p.thought);\n\n if (funcCalls.length > 0) {\n messages.push({\n role: \"assistant\",\n content: null,\n tool_calls: funcCalls.map((p, i) => ({\n id: `call_gemini_${p.functionCall!.name}_${i}`,\n type: \"function\" as const,\n function: {\n name: p.functionCall!.name,\n arguments: JSON.stringify(p.functionCall!.args),\n },\n })),\n });\n } else {\n const text = textParts.map((p) => p.text!).join(\"\");\n messages.push({ role: \"assistant\", content: text });\n }\n }\n }\n\n return messages;\n}\n\n/**\n * Convert toolResponse messages into ChatMessage[] for fixture matching.\n */\nfunction toolResponseToMessages(toolResponse: GeminiLiveToolResponse): ChatMessage[] {\n return toolResponse.functionResponses.map((fr, i) => ({\n role: \"tool\" as const,\n content: typeof fr.response === \"string\" ? fr.response : JSON.stringify(fr.response),\n tool_call_id: fr.id ?? `call_gemini_${fr.name}_${i}`,\n }));\n}\n\n/**\n * Convert Gemini tool definitions to ChatCompletion ToolDefinition[].\n */\nfunction convertTools(geminiTools?: GeminiLiveToolDef[]): ToolDefinition[] {\n if (!geminiTools || geminiTools.length === 0) return [];\n const decls = geminiTools.flatMap((t) => t.functionDeclarations ?? []);\n return decls.map((d) => ({\n type: \"function\" as const,\n function: {\n name: d.name,\n description: d.description,\n parameters: d.parameters,\n },\n }));\n}\n\n// ─── Main handler ───────────────────────────────────────────────────────────\n\nexport function handleWebSocketGeminiLive(\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 session: SessionState = {\n setupDone: false,\n model: defaults.model,\n tools: [],\n conversationHistory: [],\n };\n\n let pending = Promise.resolve();\n ws.on(\"message\", (raw: string) => {\n pending = pending.then(() =>\n processMessage(raw, ws, fixtures, journal, defaults, session).catch((err: unknown) => {\n const msg = err instanceof Error ? err.message : \"Internal error\";\n logger.error(`WebSocket Gemini Live error: ${msg}`);\n try {\n ws.send(\n JSON.stringify({\n error: { code: 13, message: msg, status: \"INTERNAL\" },\n }),\n );\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 session: SessionState,\n): Promise<void> {\n let parsed: GeminiLiveMessage;\n try {\n parsed = JSON.parse(raw) as GeminiLiveMessage;\n } catch {\n ws.send(\n JSON.stringify({\n error: { code: 3, message: \"Malformed JSON\", status: \"INVALID_ARGUMENT\" },\n }),\n );\n return;\n }\n\n // Handle setup message (accept both `setup` and `config` as aliases)\n const setupMsg = parsed.setup ?? parsed.config;\n if (setupMsg) {\n session.setupDone = true;\n session.model = setupMsg.model ?? defaults.model;\n session.tools = convertTools(setupMsg.tools);\n ws.send(JSON.stringify({ setupComplete: {} }));\n return;\n }\n\n // Reject messages before setup\n if (!session.setupDone) {\n ws.send(\n JSON.stringify({\n error: { code: 9, message: \"Setup required\", status: \"FAILED_PRECONDITION\" },\n }),\n );\n return;\n }\n\n // Build messages from this interaction\n let newMessages: ChatMessage[];\n\n if (parsed.clientContent) {\n if (!parsed.clientContent.turns || !Array.isArray(parsed.clientContent.turns)) {\n ws.send(\n JSON.stringify({\n error: {\n code: 3,\n message: \"Missing 'turns' in clientContent\",\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n newMessages = geminiTurnsToMessages(parsed.clientContent.turns);\n } else if (parsed.toolResponse) {\n if (\n !parsed.toolResponse.functionResponses ||\n !Array.isArray(parsed.toolResponse.functionResponses)\n ) {\n ws.send(\n JSON.stringify({\n error: {\n code: 3,\n message: \"Missing 'functionResponses' in toolResponse\",\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n newMessages = toolResponseToMessages(parsed.toolResponse);\n } else {\n ws.send(\n JSON.stringify({\n error: {\n code: 3,\n message: \"Expected clientContent or toolResponse\",\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n\n // Build completion request for fixture matching (include new messages speculatively)\n const completionReq: ChatCompletionRequest = {\n model: session.model,\n messages: [...session.conversationHistory, ...newMessages],\n stream: true,\n tools: session.tools.length > 0 ? session.tools : undefined,\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 path = WS_PATH;\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 journal.add({\n method: \"WS\",\n path,\n headers: {},\n body: completionReq,\n response: { status: 404, fixture: null },\n });\n ws.close(1008, \"Strict mode: no fixture matched\");\n return;\n }\n journal.add({\n method: \"WS\",\n path,\n headers: {},\n body: completionReq,\n response: { status: 404, fixture: null },\n });\n ws.send(\n JSON.stringify({\n error: { code: 5, message: \"No fixture matched\", status: \"NOT_FOUND\" },\n }),\n );\n return;\n }\n\n // Commit messages to conversation history only after successful fixture match\n session.conversationHistory.push(...newMessages);\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,\n headers: {},\n body: completionReq,\n response: { status, fixture },\n });\n ws.send(\n JSON.stringify({\n error: {\n code: httpToGrpc(status),\n message: response.error.message,\n status: response.error.type ?? \"INTERNAL\",\n },\n }),\n );\n return;\n }\n\n // Audio response — single frame with inlineData and turnComplete: true\n if (isAudioResponse(response)) {\n journal.add({\n method: \"WS\",\n path,\n headers: {},\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n const audioResp = response as AudioResponse;\n let mimeType: string;\n let data: string;\n\n if (typeof audioResp.audio === \"string\") {\n mimeType = formatToMime(audioResp.format ?? \"mp3\");\n data = audioResp.audio;\n } else {\n mimeType = audioResp.audio.contentType ?? \"audio/mpeg\";\n data = audioResp.audio.b64Json;\n }\n\n ws.send(\n JSON.stringify({\n serverContent: {\n modelTurn: {\n parts: [{ inlineData: { mimeType, data } }],\n },\n turnComplete: true,\n },\n }),\n );\n\n session.conversationHistory.push({\n role: \"assistant\",\n content: \"[audio]\",\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,\n headers: {},\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n const content = response.content;\n const chunkList: string[] = [];\n for (let i = 0; i < content.length; i += chunkSize) {\n chunkList.push(content.slice(i, i + chunkSize));\n }\n\n const interruption = createInterruptionSignal(fixture);\n let interrupted = false;\n\n // Stream text content chunks (turnComplete omitted — sent as a separate message later)\n if (content.length === 0) {\n if (!ws.isClosed) {\n ws.send(\n JSON.stringify({\n serverContent: {\n modelTurn: { parts: [{ text: \"\" }] },\n },\n }),\n );\n }\n } else {\n for (let i = 0; i < chunkList.length; i++) {\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\n ws.send(\n JSON.stringify({\n serverContent: {\n modelTurn: { parts: [{ text: chunkList[i] }] },\n },\n }),\n );\n interruption?.tick();\n if (interruption?.signal.aborted) {\n interrupted = true;\n break;\n }\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 // Pre-compute tool calls with stable IDs so wire message and history match\n const resolvedToolCalls = response.toolCalls.map((tc) => ({\n ...tc,\n resolvedId: tc.id ?? generateToolCallId(),\n }));\n\n // Send tool calls\n if (!ws.isClosed) {\n if (latency > 0) await delay(latency, interruption?.signal);\n if (interruption?.signal.aborted) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n interruption?.cleanup();\n return;\n }\n\n const functionCalls = resolvedToolCalls.map((tc) => {\n let argsObj: Record<string, unknown>;\n try {\n argsObj = JSON.parse(tc.arguments || \"{}\") as Record<string, unknown>;\n } catch {\n defaults.logger.warn(\n `Malformed JSON in fixture tool call arguments for \"${tc.name}\": ${tc.arguments}`,\n );\n argsObj = {};\n }\n return {\n name: tc.name,\n args: argsObj,\n id: tc.resolvedId,\n };\n });\n\n ws.send(JSON.stringify({ toolCall: { functionCalls } }));\n interruption?.tick();\n }\n\n if (interruption?.signal.aborted) {\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 // Send turnComplete\n if (!ws.isClosed) {\n ws.send(\n JSON.stringify({\n serverContent: { turnComplete: true },\n }),\n );\n }\n\n // Add to conversation history using the same resolved IDs from the wire message\n session.conversationHistory.push({\n role: \"assistant\",\n content: content || null,\n tool_calls: resolvedToolCalls.map((tc) => ({\n id: tc.resolvedId,\n type: \"function\" as const,\n function: {\n name: tc.name,\n arguments: tc.arguments,\n },\n })),\n });\n return;\n }\n\n // Text response — stream chunks with serverContent\n if (isTextResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path,\n headers: {},\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n const content = response.content;\n\n if (content.length === 0) {\n if (ws.isClosed) return;\n // Empty content: send empty modelTurn, then separate turnComplete\n ws.send(\n JSON.stringify({\n serverContent: {\n modelTurn: { parts: [{ text: \"\" }] },\n },\n }),\n );\n ws.send(\n JSON.stringify({\n serverContent: { turnComplete: true },\n }),\n );\n return;\n }\n\n // Chunk the content\n const chunks: string[] = [];\n for (let i = 0; i < content.length; i += chunkSize) {\n chunks.push(content.slice(i, i + chunkSize));\n }\n\n const interruption = createInterruptionSignal(fixture);\n let interrupted = false;\n\n // Stream content chunks without turnComplete (sent separately after)\n for (let i = 0; i < chunks.length; i++) {\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\n ws.send(\n JSON.stringify({\n serverContent: {\n modelTurn: { parts: [{ text: chunks[i] }] },\n },\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 // Send separate turnComplete message\n if (!ws.isClosed) {\n ws.send(\n JSON.stringify({\n serverContent: { turnComplete: true },\n }),\n );\n }\n\n // Add assistant response to conversation history\n session.conversationHistory.push({ role: \"assistant\", content });\n return;\n }\n\n // Tool call response\n if (isToolCallResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path,\n headers: {},\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n const interruption = createInterruptionSignal(fixture);\n\n if (ws.isClosed) {\n interruption?.cleanup();\n return;\n }\n if (latency > 0) await delay(latency, interruption?.signal);\n if (interruption?.signal.aborted) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n interruption?.cleanup();\n return;\n }\n if (ws.isClosed) {\n interruption?.cleanup();\n return;\n }\n\n const functionCalls = response.toolCalls.map((tc, i) => {\n let argsObj: Record<string, unknown>;\n try {\n argsObj = JSON.parse(tc.arguments || \"{}\") as Record<string, unknown>;\n } catch {\n defaults.logger.warn(\n `Malformed JSON in fixture tool call arguments for \"${tc.name}\": ${tc.arguments}`,\n );\n argsObj = {};\n }\n return {\n name: tc.name,\n args: argsObj,\n id: tc.id ?? `call_gemini_${tc.name}_${i}`,\n };\n });\n\n ws.send(JSON.stringify({ toolCall: { functionCalls } }));\n interruption?.tick();\n\n if (interruption?.signal.aborted) {\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 // Send turnComplete after tool call\n if (!ws.isClosed) {\n ws.send(\n JSON.stringify({\n serverContent: { turnComplete: true },\n }),\n );\n }\n\n // Add assistant tool_calls to conversation history\n session.conversationHistory.push({\n role: \"assistant\",\n content: null,\n tool_calls: response.toolCalls.map((tc, i) => ({\n id: tc.id ?? `call_gemini_${tc.name}_${i}`,\n type: \"function\" as const,\n function: {\n name: tc.name,\n arguments: tc.arguments,\n },\n })),\n });\n return;\n }\n\n // Unknown response type\n journal.add({\n method: \"WS\",\n path,\n headers: {},\n body: completionReq,\n response: { status: 500, fixture },\n });\n ws.send(\n JSON.stringify({\n error: {\n code: 13,\n message: \"Fixture response did not match any known type\",\n status: \"INTERNAL\",\n },\n }),\n );\n}\n"],"mappings":"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"}
@@ -1,10 +1,23 @@
1
+ const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
1
2
  const require_helpers = require('./helpers.cjs');
2
3
  const require_journal = 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');
7
+ let node_crypto = require("node:crypto");
6
8
 
7
9
  //#region src/ws-realtime.ts
10
+ /**
11
+ * WebSocket handler for OpenAI Realtime API.
12
+ *
13
+ * Accepts Realtime API messages (session.update, conversation.item.create,
14
+ * response.create) over WebSocket and sends back Realtime API events as
15
+ * individual WebSocket text frames.
16
+ */
17
+ /** Generate a Realtime-API-style ID with underscore separator (e.g. event_xxx, item_xxx). */
18
+ function realtimeId(prefix) {
19
+ return `${prefix}_${(0, node_crypto.randomBytes)(12).toString("base64url")}`;
20
+ }
8
21
  function realtimeItemsToMessages(items, instructions, logger) {
9
22
  const messages = [];
10
23
  if (instructions) messages.push({
@@ -45,7 +58,7 @@ function realtimeItemsToMessages(items, instructions, logger) {
45
58
  function evt(type, extra = {}) {
46
59
  return JSON.stringify({
47
60
  type,
48
- event_id: require_helpers.generateId("evt"),
61
+ event_id: realtimeId("event"),
49
62
  ...extra
50
63
  });
51
64
  }
@@ -58,7 +71,7 @@ function buildErrorRealtimeEvent(message, type = "invalid_request_error", code)
58
71
  }
59
72
  function handleWebSocketRealtime(ws, fixtures, journal, defaults) {
60
73
  const { logger } = defaults;
61
- const sessionId = require_helpers.generateId("sess");
74
+ const sessionId = realtimeId("sess");
62
75
  const session = {
63
76
  model: defaults.model,
64
77
  modalities: ["text"],
@@ -73,7 +86,12 @@ function handleWebSocketRealtime(ws, fixtures, journal, defaults) {
73
86
  const conversationItems = [];
74
87
  ws.send(evt("session.created", { session: {
75
88
  id: sessionId,
76
- ...session
89
+ object: "realtime.session",
90
+ ...session,
91
+ expires_at: Math.floor(Date.now() / 1e3) + 3600,
92
+ max_response_output_tokens: "inf",
93
+ input_audio_transcription: null,
94
+ tool_choice: "auto"
77
95
  } }));
78
96
  let pending = Promise.resolve();
79
97
  ws.on("message", (raw) => {
@@ -103,7 +121,14 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session, con
103
121
  if (parsed.session.model !== void 0) session.model = parsed.session.model;
104
122
  if (parsed.session.temperature !== void 0) session.temperature = parsed.session.temperature;
105
123
  }
106
- ws.send(evt("session.updated", { session: { ...session } }));
124
+ ws.send(evt("session.updated", { session: {
125
+ ...session,
126
+ object: "realtime.session",
127
+ expires_at: Math.floor(Date.now() / 1e3) + 3600,
128
+ max_response_output_tokens: "inf",
129
+ input_audio_transcription: null,
130
+ tool_choice: "auto"
131
+ } }));
107
132
  return;
108
133
  }
109
134
  if (msgType === "conversation.item.create") {
@@ -112,9 +137,13 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session, con
112
137
  return;
113
138
  }
114
139
  const item = parsed.item;
115
- if (!item.id) item.id = require_helpers.generateId("item");
140
+ if (!item.id) item.id = realtimeId("item");
141
+ const previousId = conversationItems.length > 0 ? conversationItems[conversationItems.length - 1].id ?? null : null;
116
142
  conversationItems.push(item);
117
- ws.send(evt("conversation.item.created", { item }));
143
+ ws.send(evt("conversation.item.created", {
144
+ previous_item_id: previousId,
145
+ item
146
+ }));
118
147
  return;
119
148
  }
120
149
  if (msgType === "response.create") {
@@ -130,7 +159,7 @@ async function handleResponseCreate(ws, fixtures, journal, defaults, session, co
130
159
  };
131
160
  const testId = defaults.testId ?? require_journal.DEFAULT_TEST_ID;
132
161
  const fixture = require_router.matchFixture(fixtures, completionReq, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform);
133
- const responseId = require_helpers.generateId("resp");
162
+ const responseId = realtimeId("resp");
134
163
  if (fixture) journal.incrementFixtureMatchCount(fixture, fixtures, testId);
135
164
  if (!fixture) {
136
165
  if (defaults.strict) {
@@ -150,11 +179,15 @@ async function handleResponseCreate(ws, fixtures, journal, defaults, session, co
150
179
  });
151
180
  ws.send(evt("response.created", { response: {
152
181
  id: responseId,
182
+ object: "realtime.response",
153
183
  status: "failed",
154
- output: []
184
+ status_details: null,
185
+ output: [],
186
+ usage: null
155
187
  } }));
156
188
  ws.send(evt("response.done", { response: {
157
189
  id: responseId,
190
+ object: "realtime.response",
158
191
  status: "failed",
159
192
  output: [],
160
193
  status_details: {
@@ -164,6 +197,11 @@ async function handleResponseCreate(ws, fixtures, journal, defaults, session, co
164
197
  type: "invalid_request_error",
165
198
  code: "no_fixture_match"
166
199
  }
200
+ },
201
+ usage: {
202
+ total_tokens: 0,
203
+ input_tokens: 0,
204
+ output_tokens: 0
167
205
  }
168
206
  } }));
169
207
  return;
@@ -185,11 +223,15 @@ async function handleResponseCreate(ws, fixtures, journal, defaults, session, co
185
223
  });
186
224
  ws.send(evt("response.created", { response: {
187
225
  id: responseId,
226
+ object: "realtime.response",
188
227
  status: "failed",
189
- output: []
228
+ status_details: null,
229
+ output: [],
230
+ usage: null
190
231
  } }));
191
232
  ws.send(evt("response.done", { response: {
192
233
  id: responseId,
234
+ object: "realtime.response",
193
235
  status: "failed",
194
236
  output: [],
195
237
  status_details: {
@@ -199,6 +241,11 @@ async function handleResponseCreate(ws, fixtures, journal, defaults, session, co
199
241
  type: response.error.type,
200
242
  code: response.error.code
201
243
  }
244
+ },
245
+ usage: {
246
+ total_tokens: 0,
247
+ input_tokens: 0,
248
+ output_tokens: 0
202
249
  }
203
250
  } }));
204
251
  return;
@@ -214,13 +261,14 @@ async function handleResponseCreate(ws, fixtures, journal, defaults, session, co
214
261
  fixture
215
262
  }
216
263
  });
217
- const itemId = require_helpers.generateId("item");
264
+ const itemId = realtimeId("item");
218
265
  const contentIndex = 0;
219
266
  const outputIndex = 0;
220
267
  const outputItem = {
221
268
  id: itemId,
222
269
  type: "message",
223
270
  role: "assistant",
271
+ status: "completed",
224
272
  content: [{
225
273
  type: "text",
226
274
  text: response.content
@@ -228,8 +276,11 @@ async function handleResponseCreate(ws, fixtures, journal, defaults, session, co
228
276
  };
229
277
  ws.send(evt("response.created", { response: {
230
278
  id: responseId,
279
+ object: "realtime.response",
231
280
  status: "in_progress",
232
- output: []
281
+ status_details: null,
282
+ output: [],
283
+ usage: null
233
284
  } }));
234
285
  ws.send(evt("response.output_item.added", {
235
286
  response_id: responseId,
@@ -238,6 +289,7 @@ async function handleResponseCreate(ws, fixtures, journal, defaults, session, co
238
289
  id: itemId,
239
290
  type: "message",
240
291
  role: "assistant",
292
+ status: "in_progress",
241
293
  content: []
242
294
  }
243
295
  }));
@@ -309,8 +361,14 @@ async function handleResponseCreate(ws, fixtures, journal, defaults, session, co
309
361
  }));
310
362
  ws.send(evt("response.done", { response: {
311
363
  id: responseId,
364
+ object: "realtime.response",
312
365
  status: "completed",
313
- output: [outputItem]
366
+ output: [outputItem],
367
+ usage: {
368
+ total_tokens: 0,
369
+ input_tokens: 0,
370
+ output_tokens: 0
371
+ }
314
372
  } }));
315
373
  conversationItems.push({
316
374
  type: "message",
@@ -336,8 +394,11 @@ async function handleResponseCreate(ws, fixtures, journal, defaults, session, co
336
394
  });
337
395
  ws.send(evt("response.created", { response: {
338
396
  id: responseId,
397
+ object: "realtime.response",
339
398
  status: "in_progress",
340
- output: []
399
+ status_details: null,
400
+ output: [],
401
+ usage: null
341
402
  } }));
342
403
  const outputItems = [];
343
404
  const interruption = require_interruption.createInterruptionSignal(fixture);
@@ -345,10 +406,11 @@ async function handleResponseCreate(ws, fixtures, journal, defaults, session, co
345
406
  for (let tcIdx = 0; tcIdx < response.toolCalls.length; tcIdx++) {
346
407
  const tc = response.toolCalls[tcIdx];
347
408
  const callId = tc.id ?? require_helpers.generateToolCallId();
348
- const itemId = require_helpers.generateId("item");
409
+ const itemId = realtimeId("item");
349
410
  const outputItem = {
350
411
  id: itemId,
351
412
  type: "function_call",
413
+ status: "completed",
352
414
  call_id: callId,
353
415
  name: tc.name,
354
416
  arguments: tc.arguments
@@ -359,6 +421,7 @@ async function handleResponseCreate(ws, fixtures, journal, defaults, session, co
359
421
  item: {
360
422
  id: itemId,
361
423
  type: "function_call",
424
+ status: "in_progress",
362
425
  call_id: callId,
363
426
  name: tc.name,
364
427
  arguments: ""
@@ -413,8 +476,14 @@ async function handleResponseCreate(ws, fixtures, journal, defaults, session, co
413
476
  if (ws.isClosed) return;
414
477
  ws.send(evt("response.done", { response: {
415
478
  id: responseId,
479
+ object: "realtime.response",
416
480
  status: "completed",
417
- output: outputItems
481
+ output: outputItems,
482
+ usage: {
483
+ total_tokens: 0,
484
+ input_tokens: 0,
485
+ output_tokens: 0
486
+ }
418
487
  } }));
419
488
  for (const item of outputItems) conversationItems.push(item);
420
489
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"ws-realtime.cjs","names":["generateToolCallId","generateId","DEFAULT_TEST_ID","matchFixture","resolveResponse","isErrorResponse","isTextResponse","createInterruptionSignal","delay","isToolCallResponse"],"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 resolveResponse,\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 = 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: { 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.cjs","names":["generateToolCallId","DEFAULT_TEST_ID","matchFixture","resolveResponse","isErrorResponse","isTextResponse","createInterruptionSignal","delay","isToolCallResponse"],"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} 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 },\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 },\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(\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 },\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 (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: {\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 +1 @@
1
- {"version":3,"file":"ws-realtime.d.cts","names":[],"sources":["../src/ws-realtime.ts"],"sourcesContent":[],"mappings":";;;;;;;iBAiIgB,uBAAA,KACV,+BACM,oBACD;;;;UAKC;;2BAEiB,0BAA0B"}
1
+ {"version":3,"file":"ws-realtime.d.cts","names":[],"sources":["../src/ws-realtime.ts"],"sourcesContent":[],"mappings":";;;;;;;iBAsIgB,uBAAA,KACV,+BACM,oBACD;;;;UAKC;;2BAEiB,0BAA0B"}
@@ -1 +1 @@
1
- {"version":3,"file":"ws-realtime.d.ts","names":[],"sources":["../src/ws-realtime.ts"],"sourcesContent":[],"mappings":";;;;;;;iBAiIgB,uBAAA,KACV,+BACM,oBACD;;;;UAKC;;2BAEiB,0BAA0B"}
1
+ {"version":3,"file":"ws-realtime.d.ts","names":[],"sources":["../src/ws-realtime.ts"],"sourcesContent":[],"mappings":";;;;;;;iBAsIgB,uBAAA,KACV,+BACM,oBACD;;;;UAKC;;2BAEiB,0BAA0B"}