SPB Git

Repository Public

websocket-mode.md 32.4 KB 971 lines markdown

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)
Files main
Menu

Theme