// Low-level OpenAI Responses WebSocket protocol helpers. Session pooling, // fallback, and continuation state intentionally live above this file. import WebSocket from "ws" import { ProviderError } from "@/provider/error" export const PROTOCOL_HEADER = "responses_websockets=2026-02-06" export interface ConnectResponsesWebSocketOptions { url: string headers: Record timeout?: number signal?: AbortSignal } export interface StreamResponsesWebSocketOptions { socket: WebSocket body: Record idleTimeout?: number signal?: AbortSignal onFirstEvent?: () => void onComplete?: (event: Record) => void onTerminal?: (event: Record) => void onRetryableTerminal?: (event: Record) => Promise onConnectionInvalid?: (error: ProviderError.ResponseStreamError) => void onAbort?: (error: Error) => void } export function toWebSocketUrl(url: string) { return url.replace(/^http/, "ws") } export function normalizeHeaders(headers: HeadersInit | undefined): Record { const result: Record = {} if (!headers) return result if (headers instanceof Headers) { headers.forEach((value, key) => { result[key.toLowerCase()] = value }) return result } if (Array.isArray(headers)) { for (const [key, value] of headers) { result[key.toLowerCase()] = value } return result } for (const [key, value] of Object.entries(headers)) { if (value != null) result[key.toLowerCase()] = value } return result } export function isAbortError(error: unknown): error is DOMException { return error instanceof DOMException && error.name === "AbortError" } export function connectResponsesWebSocket(options: ConnectResponsesWebSocketOptions) { return new Promise((resolve, reject) => { if (options.signal?.aborted) { reject(abortError(options.signal)) return } const headers: Record = { ...options.headers, "openai-beta": options.headers["openai-beta"] ?? PROTOCOL_HEADER, } delete headers["content-length"] const socket = new WebSocket(options.url, { headers }) const timeout = options.timeout ? setTimeout(() => { cleanup() socket.on("error", () => {}) socket.terminate() reject(new Error("WebSocket connect timed out")) }, options.timeout) : undefined function cleanup() { if (timeout) clearTimeout(timeout) socket.off("open", onOpen) socket.off("error", onError) socket.off("close", onClose) options.signal?.removeEventListener("abort", onAbort) } function onOpen() { cleanup() resolve(socket) } function onError(error: Error) { socket.on("error", () => {}) cleanup() reject(error) } function onClose(code: number, reason: Buffer) { cleanup() reject(new Error(closeMessage("WebSocket closed before open", code, reason))) } function onAbort() { cleanup() socket.on("error", () => {}) socket.terminate() reject(abortError(options.signal)) } socket.once("open", onOpen) socket.once("error", onError) socket.once("close", onClose) options.signal?.addEventListener("abort", onAbort, { once: true }) }) } export function streamResponsesWebSocket(options: StreamResponsesWebSocketOptions) { const encoder = new TextEncoder() let socket = options.socket let controller: ReadableStreamDefaultController | undefined let cleanupSocket = () => {} let completed = false let emitted = false let idleTimer: ReturnType | undefined function cleanup() { if (idleTimer) clearTimeout(idleTimer) cleanupSocket() options.signal?.removeEventListener("abort", onAbort) } function terminateSocket(target = socket) { target.on("error", () => {}) target.terminate() } function closeCompleted() { cleanup() controller?.enqueue(encoder.encode("data: [DONE]\n\n")) controller?.close() } function invalidate(error: ProviderError.ResponseStreamError) { if (completed) return completed = true cleanup() options.onConnectionInvalid?.(error) controller?.error(error) } function resetIdleTimeout(message: string) { if (completed) return if (!options.idleTimeout) return if (idleTimer) clearTimeout(idleTimer) idleTimer = setTimeout(() => invalidate(new ProviderError.ResponseStreamError(message)), options.idleTimeout) if (typeof idleTimer === "object" && "unref" in idleTimer && typeof idleTimer.unref === "function") { idleTimer.unref() } } async function onMessage(data: WebSocket.RawData, isBinary: boolean) { if (completed) return if (isBinary) { invalidate(new ProviderError.ResponseStreamError("Unexpected binary WebSocket frame")) return } const text = data.toString() const event = (() => { try { const parsed = JSON.parse(text) return typeof parsed === "object" && parsed !== null ? parsed : undefined } catch { return undefined } })() if (event?.type === "error" && !emitted && options.onRetryableTerminal) { cleanupSocket() if (idleTimer) clearTimeout(idleTimer) idleTimer = undefined try { const next = await options.onRetryableTerminal(event) if (completed) { if (next) terminateSocket(next) return } if (next) { attach(next) return } } catch (error) { invalidate( new ProviderError.ResponseStreamError(error instanceof Error ? error.message : String(error), { cause: error, }), ) return } } if (!emitted) options.onFirstEvent?.() controller?.enqueue( encoder.encode( `${text .split(/\r?\n/) .map((line) => `data: ${line}`) .join("\n")}\n\n`, ), ) emitted = true resetIdleTimeout("idle timeout waiting for websocket") if (!event) return if (event.type === "response.completed" || event.type === "response.done") { completed = true options.onComplete?.(event) options.onTerminal?.(event) closeCompleted() return } if (event.type === "response.failed" || event.type === "response.incomplete" || event.type === "error") { completed = true options.onTerminal?.(event) closeCompleted() } } function onError(error: Error) { invalidate(new ProviderError.ResponseStreamError(error.message, { cause: error })) } function onClose(code: number, reason: Buffer) { if (completed) return invalidate( new ProviderError.ResponseStreamError(closeMessage("WebSocket closed before response.completed", code, reason)), ) } function onAbort() { const error = abortError(options.signal) if (completed) return completed = true cleanup() terminateSocket() options.onAbort?.(error) controller?.error(error) } function onCancel(reason: unknown) { if (completed) return completed = true cleanup() terminateSocket() options.onAbort?.(cancelError(reason)) } function attach(next: WebSocket) { cleanupSocket() socket = next socket.on("message", onMessage) socket.once("error", onError) socket.once("close", onClose) cleanupSocket = () => { socket.off("message", onMessage) socket.off("error", onError) socket.off("close", onClose) } const { stream: _stream, background: _background, ...payload } = options.body resetIdleTimeout("idle timeout sending websocket request") socket.send(JSON.stringify({ type: "response.create", ...payload }), (error) => { if (completed) return resetIdleTimeout("idle timeout waiting for websocket") if (error) invalidate(new ProviderError.ResponseStreamError(error.message, { cause: error })) }) } return new Response( new ReadableStream({ start(next) { controller = next options.signal?.addEventListener("abort", onAbort, { once: true }) if (options.signal?.aborted) { onAbort() return } attach(socket) }, cancel(reason) { onCancel(reason) }, }), { status: 200, headers: { "content-type": "text/event-stream" }, }, ) } function cancelError(reason: unknown) { if (isAbortError(reason)) return reason if (reason instanceof Error) return reason return new DOMException(typeof reason === "string" ? reason : "Aborted", "AbortError") } function abortError(signal: AbortSignal | undefined) { const reason = signal?.reason if (isAbortError(reason)) return reason return new DOMException(reason instanceof Error ? reason.message : "Aborted", "AbortError") } function closeMessage(message: string, code: number, reason: Buffer) { const details = [`code ${code}`] if (code === 1009) details.push("message too big") if (reason.length > 0) details.push(reason.toString()) return `${message} (${details.join(": ")})` } export * as OpenAIWebSocket from "./ws"