Repository Public
1# WebSocket Mode23> For the complete documentation index, see [llms.txt](/llms.txt). Markdown versions of documentation pages are available by appending `.md` to the page URL.45The Responses API supports a WebSocket mode for long-running, tool-call-heavy workflows. Beyond lowering latency, `stream_id` enables WebSocket multiplexing: one persistent connection to `/v1/responses` can run parallel conversations and fork an existing conversation onto a new stream. Continue each turn by sending only new input items plus `previous_response_id`.67WebSocket mode is compatible with both Zero Data Retention (ZDR) and `store=false`.89## Why use WebSocket mode1011WebSocket mode is most useful when a workflow involves many model-tool round trips (for example, agentic coding or orchestration loops with repeated tool calls).1213Because the connection stays open and each turn sends only incremental input, WebSocket mode reduces per-turn continuation overhead and improves end-to-end latency across long chains. For rollouts with 20+ tool calls, we have seen up to roughly 40% faster end-to-end execution.1415## Connect and create responses1617Install the WebSocket dependencies with `pip install "openai[realtime]>=3.8.0"` for Python, `npm install openai@^7.10.0 ws` for JavaScript, or `gem install openai async-websocket` for Ruby.1819In WebSocket mode, start each turn by sending a `response.create` event from the client. The payload mirrors the normal [Responses create body](https://developers.openai.com/api/reference/resources/responses/methods/create), except that transport-specific fields like `stream` and `background` are not used.2021```javascript22import OpenAI from "openai";23import { ResponsesWS } from "openai/resources/responses/ws";2425const client = new OpenAI();2627const ws = new ResponsesWS(client);28try {29 ws.send({30 type: "response.create",31 stream_id: "main",32 model: "gpt-6-astra",33 store: false,34 input: [35 {36 type: "message",37 role: "user",38 content: [{ type: "input_text", text: "Find fizz_buzz()" }],39 },40 ],41 tools: [],42 });43 let completed = false;44 for await (const event of ws) {45 if (event.type === "error") throw event.error;46 if (event.type !== "message") continue;47 const message = event.message;48 if (message.type === "response.output_text.delta") {49 process.stdout.write(message.delta);50 } else if (message.type === "response.completed") {51 completed = true;52 break;53 } else if (54 message.type === "response.failed" ||55 message.type === "response.incomplete"56 ) {57 throw new Error(JSON.stringify(message));58 }59 }60 if (!completed)61 throw new Error("Connection closed before the response finished.");62} finally {63 ws.close();64}65```6667```python68from openai import OpenAI6970client = OpenAI()7172with client.responses.connect() as connection:73 connection.response.create(74 stream_id="main",75 model="gpt-6-astra",76 store=False,77 input=[78 {79 "type": "message",80 "role": "user",81 "content": [{"type": "input_text", "text": "Find fizz_buzz()"}],82 }83 ],84 tools=[],85 )86 for event in connection:87 if event.type == "response.completed":88 print(event.response.output_text)89 break90 if event.type in {"response.failed", "response.incomplete", "error"}:91 raise RuntimeError(event.to_json())92```9394```ruby95require "async"96require "openai"9798def wait_for_response(connection)99 while (event = connection.receive)100 case event.type.to_s101 when "response.completed" then return event.response102 when "response.failed", "response.incomplete", "error"103 raise "Response failed: #{event.to_json}"104 end105 end106 raise "Connection closed before the response finished"107end108109client = OpenAI::Client.new110Sync do |task|111 task.with_timeout(120) do112 client.responses.connect(request_options: { timeout: 10 }) do |connection|113 connection.response.create(114 stream_id: "main", model: "gpt-6-astra", store: false,115 input: [116 {117 role: "user",118 content: "Find fizz_buzz()"119 }120 ], tools: []121 )122 puts(wait_for_response(connection).output_text)123 end124 end125end126```127128129Clients can optionally warm up request state by sending `response.create` with `generate: false`. This is useful when you already know the tools, instructions, and/or custom messages you plan to send with an upcoming turn. `generate: false` does not return a model output, but prepares request state so the next generated turn can start faster. The warmup request returns a response ID that you can chain from with `previous_response_id`, including on later turns in a response chain. The next section explains how to continue a session using `previous_response_id` and incremental inputs.130131## Continue with incremental inputs132133To add user instructions while a response is still running, use [Mid-turn steering](https://developers.openai.com/api/docs/guides/steering). Steering preserves completed work and includes the new instructions in a continuation. Use the following `response.create` pattern for ordinary between-turn continuation and tool results.134135To continue a run, send another `response.create` with:136137- `previous_response_id` set to the prior response ID.138- `input` containing only new items (for example, tool outputs and the next user message).139140```javascript141import OpenAI from "openai";142import { ResponsesWS } from "openai/resources/responses/ws";143144const client = new OpenAI();145const model = "gpt-6-astra";146147const tools = [148 {149 type: "function",150 name: "get_test_results",151 description: "Return a local demo test result.",152 parameters: { type: "object", properties: {}, additionalProperties: false },153 strict: true,154 },155];156157async function waitForResponse(ws) {158 for await (const event of ws) {159 if (event.type === "error") throw event.error;160 if (event.type !== "message") continue;161 const message = event.message;162 if (message.type === "response.output_text.delta") {163 process.stdout.write(message.delta);164 } else if (message.type === "response.completed") {165 return message.response;166 } else if (167 message.type === "response.failed" ||168 message.type === "response.incomplete"169 ) {170 throw new Error(JSON.stringify(message));171 }172 }173 throw new Error("Connection closed before the response finished.");174}175176const ws = new ResponsesWS(client);177try {178 ws.send({179 type: "response.create",180 stream_id: "main",181 model,182 store: false,183 input: "Find the failing test and suggest a fix.",184 tools,185 tool_choice: { type: "function", name: "get_test_results" },186 parallel_tool_calls: false,187 });188 const first = await waitForResponse(ws);189 const call = first.output.find((item) => item.type === "function_call");190 if (!call || call.name !== "get_test_results") {191 throw new Error("Expected a get_test_results function call.");192 }193 const result = {194 test: "test_fizz_buzz",195 failure: 'Expected "FizzBuzz" for 15, got "Fizz".',196 };197198 // Continue on the same socket with the actual response and tool-call IDs.199 ws.send({200 type: "response.create",201 stream_id: "main",202 model,203 store: false,204 previous_response_id: first.id,205 input: [206 {207 type: "function_call_output",208 call_id: call.call_id,209 output: JSON.stringify(result),210 },211 { role: "user", content: "Now optimize it." },212 ],213 tools,214 tool_choice: "none",215 });216 await waitForResponse(ws);217} finally {218 ws.close();219}220```221222```python223import json224225from openai import OpenAI226from openai.resources.responses.responses import ResponsesConnection227from openai.types.responses import FunctionToolParam, Response228229client = OpenAI()230model = "gpt-6-astra"231tools: list[FunctionToolParam] = [232 {233 "type": "function",234 "name": "get_test_results",235 "description": "Read the demo test results.",236 "parameters": {237 "type": "object",238 "properties": {},239 "required": [],240 "additionalProperties": False,241 },242 "strict": True,243 }244]245246247def get_test_results():248 # Demo data. Replace this function with your test runner.249 return {250 "test": "test_fizz_buzz",251 "failure": 'Expected "FizzBuzz" for 15, got "Fizz".',252 }253254255def wait_for_response(connection: ResponsesConnection) -> Response:256 for event in connection:257 if event.type == "response.completed":258 return event.response259 if event.type in {"response.failed", "response.incomplete", "error"}:260 raise RuntimeError(event.to_json())261 raise RuntimeError("Connection closed before the response finished.")262263264with client.responses.connect() as connection:265 connection.response.create(266 stream_id="main",267 model=model,268 store=False,269 input="Find the failing test and suggest a fix.",270 tools=tools,271 tool_choice={"type": "function", "name": "get_test_results"},272 parallel_tool_calls=False,273 )274 response = wait_for_response(connection)275 call = next(item for item in response.output if item.type == "function_call")276 if call.name != "get_test_results" or json.loads(call.arguments) != {}:277 raise ValueError("Expected a get_test_results call with no arguments")278279 # Continue on the same connection using the actual response and tool-call IDs.280 connection.response.create(281 stream_id="main",282 model=model,283 store=False,284 previous_response_id=response.id,285 input=[286 {287 "type": "function_call_output",288 "call_id": call.call_id,289 "output": json.dumps(get_test_results()),290 },291 {"role": "user", "content": "Now optimize it."},292 ],293 tools=tools,294 tool_choice="none",295 )296 print(wait_for_response(connection).output_text)297```298299```ruby300require "async"301require "openai"302require "json"303304def wait_for_response(connection)305 while (event = connection.receive)306 case event.type.to_s307 when "response.completed" then return event.response308 when "response.failed", "response.incomplete", "error"309 raise "Response failed: #{event.to_json}"310 end311 end312 raise "Connection closed before the response finished"313end314315tools = [316 {317 type: "function",318 name: "get_test_results",319 description: "Read the demo test results.",320 parameters: {321 type: "object",322 properties: {},323 required: [],324 additionalProperties: false325 },326 strict: true327 }328]329330client = OpenAI::Client.new331Sync do |task|332 task.with_timeout(120) do333 client.responses.connect(request_options: { timeout: 10 }) do |connection|334 connection.response.create(335 stream_id: "main", model: "gpt-6-astra", store: false,336 input: "Find the failing test and suggest a fix.", tools: tools,337 tool_choice: {338 type: "function",339 name: "get_test_results"340 }, parallel_tool_calls: false341 )342 response = wait_for_response(connection)343 call = response.output.grep(OpenAI::Responses::ResponseFunctionToolCall).first344 unless call && call.name == "get_test_results" && JSON.parse(call.arguments) == {}345 raise "Expected a get_test_results call with no arguments"346 end347348 # Demo data. Replace this with your test runner.349 result = {350 test: "test_fizz_buzz",351 failure: 'Expected "FizzBuzz" for 15, got "Fizz".'352 }353 connection.response.create(354 stream_id: "main", model: "gpt-6-astra", store: false,355 previous_response_id: response.id,356 input: [357 {358 type: "function_call_output",359 call_id: call.call_id,360 output: JSON.generate(result)361 },362 {363 role: "user",364 content: "Now optimize it."365 }366 ],367 tools: tools, tool_choice: "none"368 )369 puts(wait_for_response(connection).output_text)370 end371 end372end373```374375376## How continuation works377378WebSocket mode uses the same `previous_response_id` chaining semantics as HTTP mode, but it adds a lower-latency continuation path on the active socket.379380On an active WebSocket connection, the service keeps recent previous-response state in a connection-local in-memory cache. When you use `stream_id`, each lane keeps its latest cached response, so continuing from the latest response in that lane is fast because the service can reuse connection-local state. Because the service retains previous-response state only in memory and does not write it to disk, you can use WebSocket mode in a way that is compatible with `store=false` and Zero Data Retention (ZDR).381382If a `previous_response_id` is not in the in-memory cache, behavior depends on whether you store responses:383384- With `store=true`, the service may hydrate older response IDs from persisted state when available. Continuation can still work, but it loses the in-memory latency benefit.385- With `store=false` (including ZDR), there is no persisted fallback. If the ID is uncached, the request returns `previous_response_not_found`.386387If a same-lane continuation returns a `4xx` or `5xx`, the service evicts the referenced `previous_response_id` from the connection-local cache. A cross-lane fork that returns an error preserves the shared parent so the source lane can continue.388389## Compaction and creating new responses390391If you are using compaction, there are two different continuation patterns:392393### Server-side compaction (`context_management`)394395When you enable server-side compaction (`context_management` with `compact_threshold`), compaction happens during normal `/responses` generation. In WebSocket mode, you continue the same way you normally do: send the next `response.create` with the latest `previous_response_id` and only new input items.396397### Standalone `/responses/compact`398399The standalone [`/responses/compact` endpoint](https://developers.openai.com/api/reference/resources/responses/methods/compact) returns a new compacted input window, not a response ID. After compaction, create a new response on your WebSocket connection using the compacted window as `input` (plus the next user/tool items).400401Start a new chain by omitting `previous_response_id` or setting it to `null`. Pass the compacted output as-is; do not prune the returned window.402403```javascript404import { toResponseInputItems } from "openai/lib/responses/ResponseInputItems";405406// Compact your current window with an HTTP request.407const compacted = await client.responses.compact({408 model: "gpt-6-astra",409 input: longInputItems,410});411const nextInput = toResponseInputItems(compacted.output);412nextInput.push({413 type: "message",414 role: "user",415 content: [{ type: "input_text", text: "Continue from here." }],416});417418// Start a new response on the WebSocket using the compacted window.419const ws = new ResponsesWS(client);420try {421 ws.send({422 type: "response.create",423 stream_id: "main",424 model: "gpt-6-astra",425 store: false,426 input: nextInput,427 tools: [],428 });429 let completed = false;430 for await (const event of ws) {431 if (event.type === "error") throw event.error;432 if (event.type !== "message") continue;433 const message = event.message;434 if (message.type === "response.output_text.delta") {435 process.stdout.write(message.delta);436 } else if (message.type === "response.completed") {437 completed = true;438 break;439 } else if (440 message.type === "response.failed" ||441 message.type === "response.incomplete"442 ) {443 throw new Error(JSON.stringify(message));444 }445 }446 if (!completed)447 throw new Error("Connection closed before the response finished.");448} finally {449 ws.close();450}451```452453```python454from typing import cast455456from openai import OpenAI457from openai.types.responses import ResponseInputParam458459# Compact your current window (HTTP call).460compacted = client.responses.compact(461 model="gpt-6-astra",462 input=long_input_items_array,463)464next_input = cast(465 ResponseInputParam,466 [item.to_dict() for item in compacted.output],467)468next_input.append(469 {470 "type": "message",471 "role": "user",472 "content": [{"type": "input_text", "text": "Continue from here."}],473 }474)475476# Start a new response on the WebSocket using the compacted window.477with client.responses.connect() as connection:478 connection.response.create(479 stream_id="main",480 model="gpt-6-astra",481 store=False,482 input=next_input,483 tools=[],484 )485 for event in connection:486 if event.type == "response.completed":487 print(event.response.output_text)488 break489 if event.type in {"response.failed", "response.incomplete", "error"}:490 raise RuntimeError(event.to_json())491```492493```ruby494require "async"495require "openai"496497def wait_for_response(connection)498 while (event = connection.receive)499 case event.type.to_s500 when "response.completed" then return event.response501 when "response.failed", "response.incomplete", "error"502 raise "Response failed: #{event.to_json}"503 end504 end505 raise "Connection closed before the response finished"506end507508client = OpenAI::Client.new509compacted = client.responses.compact(510 model: "gpt-6-astra",511 input: [512 {513 role: :user,514 content: "Find the failing test."515 }516 ]517)518next_input = compacted.output.map(&:to_h)519next_input << {520 role: :user,521 content: "Continue from here."522}523524Sync do |task|525 task.with_timeout(120) do526 client.responses.connect(request_options: { timeout: 10 }) do |connection|527 connection.response.create(528 stream_id: "main", model: "gpt-6-astra", store: false,529 input: next_input, tools: []530 )531 puts(wait_for_response(connection).output_text)532 end533 end534end535```536537538## Run conversations in parallel539540You can maintain parallel conversations on the same connection using the `stream_id` parameter. Send independent `response.create` events back-to-back with different `stream_id` values. The server can run them concurrently on one connection. Their events can interleave, so keep one reader loop and route each event by `stream_id`.541542A `stream_id` names an ordered lane on one WebSocket connection. Keep `stream_id` and `previous_response_id` separate:543544- `stream_id` controls where events go and which requests run in first-in, first-out order.545- `previous_response_id` controls conversation lineage.546547That separation unlocks two useful patterns.548549```text550one WebSocket connection551├─ stream_id="planner" draft a deployment plan552└─ stream_id="research" list deployment risks553```554555Requests with the same `stream_id` stay first-in, first-out and do not overlap. Requests with different `stream_id` values can run concurrently.556557### Limits per connection558559- A connection can have up to 16 active, in-flight responses across named and default lanes. The connection accepts more `response.create` events and queues them until an active response finishes.560- A connection accepts up to 32 distinct named `stream_id` values. The implicit default lane does not count toward this named-stream limit. Reuse an existing `stream_id` or open a new connection after reaching the limit.561562### Fork a conversation onto a new stream563564To branch from a completed response, send its ID as `previous_response_id` with a new `stream_id`. While that response remains available, the new stream inherits its context, and the original stream can keep going. After the fork starts, both branches can run concurrently because they use different stream IDs.565566With `store=false` (including ZDR), a cross-lane fork depends on the parent remaining in the connection-local cache. If the fork queues while the source lane advances or fails, the parent can be evicted before the fork starts, and the fork returns `previous_response_not_found`. Wait for the fork lane to emit `response.in_progress` before advancing the source lane, or retry with `previous_response_id` set to `null` and replay full input context.567568```text569main: resp_1 ──▶ resp_2 ──▶ resp_3570 ╲571critic: resp_4 ──▶ resp_5572```573574Reusing a `stream_id` without `previous_response_id` starts a new response; it does not continue the conversation.575576The key calls look like this:577578```text579# One socket, two independent conversations.580send_create(connection, "planner", "Draft a deployment plan.")581send_create(connection, "research", "List deployment risks.")582583# Fork the planner response, then continue the original branch in parallel.584send_create(585 connection,586 "critic",587 "Find gaps in this plan.",588 previous_response_id=planner_response_id,589)590wait_for_in_progress(connection, "critic")591send_create(592 connection,593 "planner",594 "Add rollback steps.",595 previous_response_id=planner_response_id,596)597```598599### Complete example600601Run parallel conversations, then fork one602603```javascript604import OpenAI from "openai";605import { ResponsesWS } from "openai/resources/responses/ws";606607const client = new OpenAI();608609const latestResponseIdByLane = new Map();610611function sendCreate(612 ws,613 streamId,614 text,615 previousResponseId = latestResponseIdByLane.get(streamId)616) {617 ws.send({618 type: "response.create",619 stream_id: streamId,620 model: "gpt-6-astra",621 store: false,622 input: [623 {624 type: "message",625 role: "user",626 content: [{ type: "input_text", text }],627 },628 ],629 previous_response_id: previousResponseId,630 });631}632633async function readMessage(events) {634 while (true) {635 const { value: event, done } = await events.next();636 if (done)637 throw new Error("Connection closed before all responses finished.");638 if (event.type === "error") throw event.error;639 if (event.type !== "message") continue;640 const message = event.message;641 if (642 message.type === "response.failed" ||643 message.type === "response.incomplete"644 ) {645 throw new Error(646 `Lane ${message.stream_id} failed: ${JSON.stringify(message)}`647 );648 }649 return message;650 }651}652653async function drainUntilComplete(events, expectedStreamIds) {654 const remaining = new Set(expectedStreamIds);655 while (remaining.size > 0) {656 const message = await readMessage(events);657 const streamId = message.stream_id;658 if (!streamId || !remaining.has(streamId)) continue;659 if (message.type === "response.completed") {660 latestResponseIdByLane.set(streamId, message.response.id);661 remaining.delete(streamId);662 }663 }664}665666async function waitForInProgress(events, streamId) {667 while (true) {668 const message = await readMessage(events);669 if (670 message.type === "response.in_progress" &&671 message.stream_id === streamId672 )673 return;674 }675}676677const ws = new ResponsesWS(client);678// Keep one iterator so events stay queued while moving between phases.679const events = ws.stream();680try {681 // Run two independent conversations in parallel.682 sendCreate(683 ws,684 "planner",685 "Draft a deployment plan for a stateless API service."686 );687 sendCreate(688 ws,689 "research",690 "List common deployment risks for a stateless API service."691 );692 await drainUntilComplete(events, new Set(["planner", "research"]));693694 // Fork the planner conversation and continue its original branch in parallel.695 const plannerResponseId = latestResponseIdByLane.get("planner");696 sendCreate(697 ws,698 "critic",699 "Find gaps in this deployment plan.",700 plannerResponseId701 );702 // Let the fork load its parent before advancing the original lane's cache.703 await waitForInProgress(events, "critic");704 sendCreate(705 ws,706 "planner",707 "Add rollback and monitoring steps to the plan.",708 plannerResponseId709 );710 await drainUntilComplete(events, new Set(["critic", "planner"]));711} finally {712 await events.return?.();713 ws.close();714}715```716717```python718from openai import OpenAI719from openai.resources.responses.responses import ResponsesConnection720721client = OpenAI()722latest_response_id_by_lane: dict[str, str] = {}723724725def send_create(726 connection: ResponsesConnection,727 stream_id: str,728 text: str,729 previous_response_id: str | None = None,730):731 if previous_response_id is None:732 previous_response_id = latest_response_id_by_lane.get(stream_id)733 connection.response.create(734 stream_id=stream_id,735 model="gpt-6-astra",736 store=False,737 input=[738 {739 "type": "message",740 "role": "user",741 "content": [{"type": "input_text", "text": text}],742 }743 ],744 previous_response_id=previous_response_id,745 )746747748def drain_until_complete(749 connection: ResponsesConnection, expected_stream_ids: set[str]750):751 completed: set[str] = set()752 for event in connection:753 stream_id = event.stream_id754 if event.type == "error" and stream_id is None:755 raise RuntimeError(f"Connection error: {event.to_json()}")756 if stream_id is None or stream_id not in expected_stream_ids:757 continue758759 if event.type == "response.completed":760 latest_response_id_by_lane[stream_id] = event.response.id761 completed.add(stream_id)762 if completed == expected_stream_ids:763 return764 elif event.type in {"response.failed", "response.incomplete", "error"}:765 raise RuntimeError(f"Lane {stream_id} failed: {event.to_json()}")766 raise RuntimeError("Connection closed before all responses finished.")767768769def wait_for_in_progress(connection: ResponsesConnection, expected_stream_id: str):770 for event in connection:771 if event.type == "error" and event.stream_id is None:772 raise RuntimeError(f"Connection error: {event.to_json()}")773 if event.stream_id != expected_stream_id:774 continue775 if event.type == "response.in_progress":776 return777 if event.type in {"response.failed", "response.incomplete", "error"}:778 raise RuntimeError(f"Lane {expected_stream_id} failed: {event.to_json()}")779 raise RuntimeError("Connection closed before the fork started.")780781782with client.responses.connect() as connection:783 # 1. Run two independent conversations in parallel.784 send_create(785 connection, "planner", "Draft a deployment plan for a stateless API service."786 )787 send_create(788 connection,789 "research",790 "List common deployment risks for a stateless API service.",791 )792 drain_until_complete(connection, {"planner", "research"})793794 # 2. Fork the planner conversation and continue the original branch in parallel.795 planner_response_id = latest_response_id_by_lane["planner"]796 send_create(797 connection,798 "critic",799 "Find gaps in this deployment plan.",800 previous_response_id=planner_response_id,801 )802 # Let the fork bind its parent before advancing the original lane.803 wait_for_in_progress(connection, "critic")804 send_create(805 connection,806 "planner",807 "Add rollback and monitoring steps to the plan.",808 previous_response_id=planner_response_id,809 )810 drain_until_complete(connection, {"critic", "planner"})811```812813```ruby814require "async"815require "openai"816require "json"817818def send_create(connection, stream_id, text, previous_response_id = nil)819 payload = {820 stream_id: stream_id,821 model: "gpt-6-astra",822 store: false,823 input: [824 {825 role: "user",826 content: text827 }828 ]829 }830 payload[:previous_response_id] = previous_response_id if previous_response_id831 connection.response.create(**payload)832end833834def read_event(connection)835 event = connection.receive or raise "Connection closed before all responses finished"836 if ["response.failed", "response.incomplete", "error"].include?(event.type.to_s)837 raise "Response failed: #{event.to_json}"838 end839840 event841end842843def drain_responses(connection, lanes, latest_ids)844 remaining = lanes.dup845 until remaining.empty?846 event = read_event(connection)847 next unless event.type.to_s == "response.completed"848849 lane = event.stream_id850 next unless remaining.include?(lane)851852 latest_ids[lane] = event.response.id853 remaining.delete(lane)854 end855end856857client = OpenAI::Client.new858Sync do |task|859 task.with_timeout(120) do860 client.responses.connect(request_options: { timeout: 10 }) do |connection|861 latest_ids = {}862 send_create(connection, "planner", "Draft a deployment plan for a stateless API service.")863 send_create(connection, "research", "List common deployment risks for a stateless API service.")864 drain_responses(connection, ["planner", "research"], latest_ids)865 parent_id = latest_ids.fetch("planner")866 send_create(connection, "critic", "Find gaps in this deployment plan.", parent_id)867 # Let the fork load its parent before advancing the original lane's cache.868 loop do869 event = read_event(connection)870 break if event.type.to_s == "response.in_progress" && event.stream_id == "critic"871 end872 send_create(connection, "planner", "Add rollback and monitoring steps.", parent_id)873 drain_responses(connection, ["critic", "planner"], latest_ids)874 puts(JSON.generate(latest_ids))875 end876 end877end878```879880881A `stream_id` must be 1–256 characters and can contain only letters, numbers, underscores (`_`), hyphens (`-`), and periods (`.`). Use it only in WebSocket `response.create` events; do not include it in HTTP `POST /v1/responses`.882883For named streams, server events include the matching `stream_id`, including terminal events and request-scoped errors.884885If you omit `stream_id`, the request uses an implicit default lane, and its events do not include `stream_id`. The default lane otherwise follows the same ordering and concurrency rules as named streams. An empty string is not a valid `stream_id`; omit the field to select the default lane.886887## Connection behavior and limits888889- Events within each response follow the existing Responses streaming event model. Events from different lanes can interleave.890- Requests with the same `stream_id` run in first-in, first-out order and don't overlap. Requests on different lanes can run concurrently.891- Connections last up to 60 minutes. Reconnect at the limit.892893## Reconnect and recover894895When a connection closes (or hits the 60-minute limit), its connection-local cache disappears for every lane. Open a new WebSocket connection and recover each lane with one of these patterns:8968971. If you stored a prior response (`store=true`) and have a valid response ID, continue that lane with `previous_response_id` and new input items.8982. If you cannot continue a lane (for example, `store=false`/ZDR or `previous_response_not_found`), start a new response by setting `previous_response_id` to `null` (or omitting it) and send the full input context for that lane's next turn.8993. If you compacted context with `/responses/compact`, use the returned compacted window as the base `input` for that new response, then append the latest user/tool items.900901## Errors to handle902903When the server can associate an error with a named lane, the error event includes `stream_id`. Other lanes can continue after a request-scoped error.904905`previous_response_not_found`906907```json908{909 "type": "error",910 "status": 400,911 "stream_id": "main",912 "error": {913 "type": "invalid_request_error",914 "code": "previous_response_not_found",915 "message": "Previous response with id 'resp_abc' not found.",916 "param": "previous_response_id"917 }918}919```920921`invalid_stream_id`922923```json924{925 "type": "error",926 "status": 400,927 "error": {928 "type": "invalid_request_error",929 "code": "invalid_stream_id",930 "message": "The 'stream_id' field must be a non-empty string with at most 256 characters and may only contain letters, numbers, underscores, hyphens, and periods.",931 "param": "stream_id"932 }933}934```935936`websocket_stream_limit_reached`937938```json939{940 "type": "error",941 "status": 400,942 "stream_id": "agent_33",943 "error": {944 "type": "invalid_request_error",945 "code": "websocket_stream_limit_reached",946 "message": "This WebSocket connection has reached its maximum number of distinct stream IDs (32). Reuse an existing stream_id or open a new WebSocket connection.",947 "param": "stream_id"948 }949}950```951952`websocket_connection_limit_reached`953954```json955{956 "type": "error",957 "error": {958 "type": "invalid_request_error",959 "code": "websocket_connection_limit_reached",960 "message": "Responses websocket connection limit reached (60 minutes). Create a new websocket connection to continue."961 },962 "status": 400963}964```965966## Related guides967968- [Conversation state](https://developers.openai.com/api/docs/guides/conversation-state)969- [Streaming API responses](https://developers.openai.com/api/docs/guides/streaming-responses)970- [Responses streaming events reference](https://developers.openai.com/api/reference/resources/responses)971- [Responses WebSocket events reference](https://developers.openai.com/api/reference/resources/responses/websocket-events)