{"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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"}