{"version":3,"file":"ws-responses.cjs","names":["responsesToCompletionRequest","DEFAULT_TEST_ID","matchFixtureDiagnostic","resolveStrictMode","strictNoMatchMessage","strictNoMatchLogLine","flattenHeaders","strictOverrideField","resolveResponse","isErrorResponse","isContentWithToolCallsResponse","buildContentWithToolCallsStreamEvents","resolveReasoningForModel","extractOverrides","createInterruptionSignal","isTextResponse","buildTextStreamEvents","isToolCallResponse","buildToolCallStreamEvents","calculateDelay","delay"],"sources":["../src/ws-responses.ts"],"sourcesContent":["/**\n * WebSocket handler for OpenAI Responses API.\n *\n * Accepts `{ type: \"response.create\", model: \"...\", input: [...] }` messages over\n * WebSocket and sends back the same Responses API SSE events as the HTTP\n * handler, but as individual WebSocket text frames.\n */\n\nimport type { ChatCompletionRequest, Fixture } from \"./types.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport {\n  responsesToCompletionRequest,\n  buildTextStreamEvents,\n  buildToolCallStreamEvents,\n  buildContentWithToolCallsStreamEvents,\n  type ResponsesSSEEvent,\n} from \"./responses.js\";\nimport {\n  isTextResponse,\n  isToolCallResponse,\n  isContentWithToolCallsResponse,\n  isErrorResponse,\n  extractOverrides,\n  resolveResponse,\n  resolveStrictMode,\n  resolveReasoningForModel,\n  strictOverrideField,\n  flattenHeaders,\n  strictNoMatchMessage,\n  strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { createInterruptionSignal } from \"./interruption.js\";\nimport { delay, calculateDelay } from \"./sse-writer.js\";\nimport { DEFAULT_TEST_ID, type Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\nimport type { WebSocketConnection } from \"./ws-framing.js\";\n\ninterface ResponseCreateMessage {\n  type: \"response.create\";\n  model?: string;\n  input?: unknown[];\n  instructions?: string;\n  tools?: unknown[];\n  tool_choice?: string | object;\n  stream?: boolean;\n  temperature?: number;\n  max_output_tokens?: number;\n  [key: string]: unknown;\n}\n\nfunction isResponseCreateMessage(msg: unknown): msg is ResponseCreateMessage {\n  return (\n    typeof msg === \"object\" &&\n    msg !== null &&\n    (msg as ResponseCreateMessage).type === \"response.create\"\n  );\n}\n\nfunction buildErrorEvent(\n  message: string,\n  type = \"invalid_request_error\",\n  code?: string,\n): ResponsesSSEEvent {\n  return {\n    type: \"error\",\n    error: { message, type, code },\n  };\n}\n\nexport function handleWebSocketResponses(\n  ws: WebSocketConnection,\n  fixtures: Fixture[],\n  journal: Journal,\n  defaults: {\n    latency: number;\n    chunkSize: number;\n    replaySpeed?: number;\n    model: string;\n    logger: Logger;\n    strict?: boolean;\n    requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n    testId?: string;\n    upgradeHeaders?: import(\"node:http\").IncomingHttpHeaders;\n  },\n): void {\n  const { logger } = defaults;\n  // Serialize message processing to prevent event interleaving\n  let pending = Promise.resolve();\n  ws.on(\"message\", (raw: string) => {\n    pending = pending.then(() =>\n      processMessage(raw, ws, fixtures, journal, defaults).catch((err: unknown) => {\n        const msg = err instanceof Error ? err.message : \"Internal error\";\n        logger.error(`WebSocket responses error: ${msg}`);\n        try {\n          ws.send(JSON.stringify(buildErrorEvent(msg, \"server_error\")));\n        } catch (sendErr) {\n          defaults.logger.debug(\n            `Failed to send error to client: ${sendErr instanceof Error ? sendErr.message : \"unknown\"}`,\n          );\n        }\n      }),\n    );\n  });\n}\n\nasync function processMessage(\n  raw: string,\n  ws: WebSocketConnection,\n  fixtures: Fixture[],\n  journal: Journal,\n  defaults: {\n    latency: number;\n    chunkSize: number;\n    replaySpeed?: number;\n    model: string;\n    logger: Logger;\n    strict?: boolean;\n    requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n    testId?: string;\n    upgradeHeaders?: import(\"node:http\").IncomingHttpHeaders;\n  },\n): Promise<void> {\n  let parsed: unknown;\n  try {\n    parsed = JSON.parse(raw);\n  } catch (parseErr) {\n    const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n    ws.send(\n      JSON.stringify(\n        buildErrorEvent(`Malformed JSON: ${detail}`, \"invalid_request_error\", \"invalid_json\"),\n      ),\n    );\n    return;\n  }\n\n  if (!isResponseCreateMessage(parsed)) {\n    ws.send(\n      JSON.stringify(\n        buildErrorEvent(\n          'Expected message type \"response.create\"',\n          \"invalid_request_error\",\n          \"invalid_message_type\",\n        ),\n      ),\n    );\n    return;\n  }\n\n  const responsesReq = {\n    model: parsed.model ?? defaults.model,\n    input: (parsed.input ?? []) as {\n      role?: string;\n      type?: string;\n      content?: string | { type: string; text?: string }[];\n      call_id?: string;\n      name?: string;\n      arguments?: string;\n      output?: string;\n      id?: string;\n    }[],\n    instructions: parsed.instructions,\n    tools: parsed.tools as\n      | {\n          type: \"function\";\n          name: string;\n          description?: string;\n          parameters?: object;\n          strict?: boolean;\n        }[]\n      | undefined,\n    tool_choice: parsed.tool_choice,\n    stream: parsed.stream,\n    temperature: parsed.temperature,\n    max_output_tokens: parsed.max_output_tokens,\n  };\n\n  const completionReq = responsesToCompletionRequest(responsesReq);\n  completionReq._endpointType = \"chat\";\n  const contextHeader = defaults.upgradeHeaders?.[\"x-aimock-context\"];\n  completionReq._context =\n    typeof contextHeader === \"string\"\n      ? contextHeader\n      : Array.isArray(contextHeader) && contextHeader.length > 0\n        ? contextHeader[0]\n        : undefined;\n  const testId = defaults.testId ?? DEFAULT_TEST_ID;\n  const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n    fixtures,\n    completionReq,\n    journal.getFixtureMatchCountsForTest(testId),\n    defaults.requestTransform,\n  );\n\n  if (fixture) {\n    journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n  }\n\n  if (!fixture) {\n    if (resolveStrictMode(defaults.strict, defaults.upgradeHeaders)) {\n      const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n      defaults.logger.error(strictNoMatchLogLine(\"WS\", \"/v1/responses\", skippedBySequenceOrTurn));\n      journal.add({\n        method: \"WS\",\n        path: \"/v1/responses\",\n        headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n        body: completionReq,\n        response: {\n          status: 503,\n          fixture: null,\n          ...strictOverrideField(defaults.strict, defaults.upgradeHeaders),\n        },\n      });\n      ws.close(1008, strictMessage);\n      return;\n    }\n    journal.add({\n      method: \"WS\",\n      path: \"/v1/responses\",\n      headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n      body: completionReq,\n      response: {\n        status: 404,\n        fixture: null,\n        ...strictOverrideField(defaults.strict, defaults.upgradeHeaders),\n      },\n    });\n    ws.send(\n      JSON.stringify(\n        buildErrorEvent(\"No fixture matched\", \"invalid_request_error\", \"no_fixture_match\"),\n      ),\n    );\n    return;\n  }\n\n  const response = await resolveResponse(fixture, completionReq);\n  const latency = fixture.latency ?? defaults.latency;\n  const chunkSize = Math.max(1, fixture.chunkSize ?? defaults.chunkSize);\n\n  // The WS path has no per-request `req.headers`; strict is resolved from the\n  // connection's upgrade headers (see the `!fixture` branch above). Used below\n  // to gate the synthesized reasoning channel on the requested model's capability.\n  const effectiveStrict = resolveStrictMode(defaults.strict, defaults.upgradeHeaders);\n\n  // Error response\n  if (isErrorResponse(response)) {\n    const status = response.status ?? 500;\n    journal.add({\n      method: \"WS\",\n      path: \"/v1/responses\",\n      headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n      body: completionReq,\n      response: { status, fixture },\n    });\n    ws.send(\n      JSON.stringify(\n        buildErrorEvent(response.error.message, response.error.type, response.error.code),\n      ),\n    );\n    return;\n  }\n\n  // Content + tool calls response (must be checked before isTextResponse / isToolCallResponse)\n  if (isContentWithToolCallsResponse(response)) {\n    const journalEntry = journal.add({\n      method: \"WS\",\n      path: \"/v1/responses\",\n      headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n      body: completionReq,\n      response: { status: 200, fixture },\n    });\n\n    const events = buildContentWithToolCallsStreamEvents(\n      response.content,\n      response.toolCalls,\n      completionReq.model,\n      chunkSize,\n      resolveReasoningForModel(\n        response.reasoning,\n        completionReq.model,\n        effectiveStrict,\n        defaults.logger,\n      ),\n      response.webSearches,\n      extractOverrides(response),\n    );\n\n    const interruption = createInterruptionSignal(fixture);\n    const completed = await sendEvents(\n      ws,\n      events,\n      latency,\n      interruption?.signal,\n      interruption?.tick,\n      fixture.recordedTimings,\n      fixture.replaySpeed ?? defaults.replaySpeed,\n    );\n    if (!completed) {\n      ws.destroy();\n      journalEntry.response.interrupted = true;\n      journalEntry.response.interruptReason = interruption?.reason();\n    }\n    interruption?.cleanup();\n    return;\n  }\n\n  // Text response\n  if (isTextResponse(response)) {\n    const journalEntry = journal.add({\n      method: \"WS\",\n      path: \"/v1/responses\",\n      headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n      body: completionReq,\n      response: { status: 200, fixture },\n    });\n\n    const events = buildTextStreamEvents(\n      response.content,\n      completionReq.model,\n      chunkSize,\n      resolveReasoningForModel(\n        response.reasoning,\n        completionReq.model,\n        effectiveStrict,\n        defaults.logger,\n      ),\n      response.webSearches,\n      extractOverrides(response),\n    );\n    const interruption = createInterruptionSignal(fixture);\n    const completed = await sendEvents(\n      ws,\n      events,\n      latency,\n      interruption?.signal,\n      interruption?.tick,\n      fixture.recordedTimings,\n      fixture.replaySpeed ?? defaults.replaySpeed,\n    );\n    if (!completed) {\n      ws.destroy();\n      journalEntry.response.interrupted = true;\n      journalEntry.response.interruptReason = interruption?.reason();\n    }\n    interruption?.cleanup();\n    return;\n  }\n\n  // Tool call response\n  if (isToolCallResponse(response)) {\n    const journalEntry = journal.add({\n      method: \"WS\",\n      path: \"/v1/responses\",\n      headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n      body: completionReq,\n      response: { status: 200, fixture },\n    });\n    const events = buildToolCallStreamEvents(\n      response.toolCalls,\n      completionReq.model,\n      chunkSize,\n      // Gate the synthesized reasoning channel on the requested model's\n      // capability, matching the WS text / content+tool branches and the HTTP\n      // tool-only path so reasoning emission is transport-independent.\n      resolveReasoningForModel(\n        response.reasoning,\n        completionReq.model,\n        effectiveStrict,\n        defaults.logger,\n      ),\n      response.webSearches,\n      extractOverrides(response),\n    );\n    const interruption = createInterruptionSignal(fixture);\n    const completed = await sendEvents(\n      ws,\n      events,\n      latency,\n      interruption?.signal,\n      interruption?.tick,\n      fixture.recordedTimings,\n      fixture.replaySpeed ?? defaults.replaySpeed,\n    );\n    if (!completed) {\n      ws.destroy();\n      journalEntry.response.interrupted = true;\n      journalEntry.response.interruptReason = interruption?.reason();\n    }\n    interruption?.cleanup();\n    return;\n  }\n\n  // Unknown response type\n  journal.add({\n    method: \"WS\",\n    path: \"/v1/responses\",\n    headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n    body: completionReq,\n    response: { status: 500, fixture },\n  });\n  ws.send(\n    JSON.stringify(\n      buildErrorEvent(\"Fixture response did not match any known type\", \"server_error\"),\n    ),\n  );\n}\n\nasync function sendEvents(\n  ws: WebSocketConnection,\n  events: ResponsesSSEEvent[],\n  latency: number,\n  signal?: AbortSignal,\n  onChunkSent?: () => void,\n  recordedTimings?: import(\"./types.js\").RecordedTimings,\n  replaySpeed?: number,\n): Promise<boolean> {\n  let eventIndex = 0;\n  for (const event of events) {\n    if (ws.isClosed) return false;\n    const chunkDelay = calculateDelay(eventIndex, undefined, latency, recordedTimings, replaySpeed);\n    if (chunkDelay > 0) await delay(chunkDelay, signal);\n    if (signal?.aborted) return false;\n    if (ws.isClosed) return false;\n    ws.send(JSON.stringify(event));\n    eventIndex++;\n    onChunkSent?.();\n    if (signal?.aborted) return false;\n  }\n  return true;\n}\n"],"mappings":"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"}