La API Responses admite un modo WebSocket para flujos de trabajo de larga duración con muchas llamadas a herramientas. Además de reducir la latencia, stream_id permite la multiplexación de WebSocket: una sola conexión persistente a /v1/responses puede ejecutar conversaciones en paralelo y crear un fork de una conversación existente en un nuevo flujo. Continúa cada turno enviando solo los nuevos elementos de entrada junto con previous_response_id.
El modo WebSocket es compatible tanto con la retención cero de datos (ZDR) como con store=false.
Por qué usar el modo WebSocket
El modo WebSocket resulta especialmente útil cuando un flujo de trabajo implica muchos intercambios entre el modelo y las herramientas (por ejemplo, codificación con agentes o bucles de orquestación con llamadas repetidas a herramientas).
Como la conexión permanece abierta y cada turno envía solo entradas incrementales, el modo WebSocket reduce la sobrecarga de continuación por turno y mejora la latencia de extremo a extremo en cadenas largas. En ejecuciones con 20 o más llamadas a herramientas, hemos observado una ejecución de extremo a extremo hasta aproximadamente un 40 % más rápida.
Conectarse y crear respuestas
Instala las dependencias de WebSocket con pip install "openai[realtime]>=3.8.0" para Python, npm install openai@^7.10.0 ws para JavaScript o gem install openai async-websocket para Ruby.
En el modo WebSocket, inicia cada turno enviando un evento response.create desde el cliente. El cuerpo de la solicitud coincide con el cuerpo habitual para crear respuestas en Responses, salvo que no se usan campos específicos del transporte, como stream y background.
import OpenAI from "openai";
import { ResponsesWS } from "openai/resources/responses/ws";
const client = new OpenAI();
const ws = new ResponsesWS(client);
try {
ws.send({
type: "response.create",
stream_id: "main",
model: "gpt-6-astra",
store: false,
input: [
{
type: "message",
role: "user",
content: [{ type: "input_text", text: "Find fizz_buzz()" }],
},
],
tools: [],
});
let completed = false;
for await (const event of ws) {
if (event.type === "error") throw event.error;
if (event.type !== "message") continue;
const message = event.message;
if (message.type === "response.output_text.delta") {
process.stdout.write(message.delta);
} else if (message.type === "response.completed") {
completed = true;
break;
} else if (
message.type === "response.failed" ||
message.type === "response.incomplete"
) {
throw new Error(JSON.stringify(message));
}
}
if (!completed)
throw new Error("Connection closed before the response finished.");
} finally {
ws.close();
}Los clientes pueden preparar de antemano el estado de la solicitud, de manera opcional, enviando response.create con generate: false. Esto es útil cuando ya conoces las herramientas, las instrucciones o los mensajes personalizados que planeas enviar en un próximo turno. generate: false no devuelve una salida del modelo, sino que prepara el estado de la solicitud para que el siguiente turno con generación pueda comenzar más rápido. La solicitud de preparación devuelve un ID de respuesta desde el que puedes encadenar respuestas con previous_response_id, incluso en turnos posteriores de una cadena de respuestas. La siguiente sección explica cómo continuar una sesión usando previous_response_id y entradas incrementales.
Continuar con entradas incrementales
Para agregar instrucciones del usuario mientras una respuesta aún está en curso, usa la Orientación durante el turno. Esta función conserva el trabajo completado e incluye las nuevas instrucciones en una continuación. Usa el siguiente patrón de response.create para la continuación habitual entre turnos y los resultados de herramientas.
Para continuar una ejecución, envía otro response.create con:
previous_response_idestablecido en el ID de la respuesta anterior.inputcon solo los elementos nuevos (por ejemplo, las salidas de herramientas y el siguiente mensaje del usuario).
import OpenAI from "openai";
import { ResponsesWS } from "openai/resources/responses/ws";
const client = new OpenAI();
const model = "gpt-6-astra";
const tools = [
{
type: "function",
name: "get_test_results",
description: "Return a local demo test result.",
parameters: { type: "object", properties: {}, additionalProperties: false },
strict: true,
},
];
async function waitForResponse(ws) {
for await (const event of ws) {
if (event.type === "error") throw event.error;
if (event.type !== "message") continue;
const message = event.message;
if (message.type === "response.output_text.delta") {
process.stdout.write(message.delta);
} else if (message.type === "response.completed") {
return message.response;
} else if (
message.type === "response.failed" ||
message.type === "response.incomplete"
) {
throw new Error(JSON.stringify(message));
}
}
throw new Error("Connection closed before the response finished.");
}
const ws = new ResponsesWS(client);
try {
ws.send({
type: "response.create",
stream_id: "main",
model,
store: false,
input: "Find the failing test and suggest a fix.",
tools,
tool_choice: { type: "function", name: "get_test_results" },
parallel_tool_calls: false,
});
const first = await waitForResponse(ws);
const call = first.output.find((item) => item.type === "function_call");
if (!call || call.name !== "get_test_results") {
throw new Error("Expected a get_test_results function call.");
}
const result = {
test: "test_fizz_buzz",
failure: 'Expected "FizzBuzz" for 15, got "Fizz".',
};
// Continue on the same socket with the actual response and tool-call IDs.
ws.send({
type: "response.create",
stream_id: "main",
model,
store: false,
previous_response_id: first.id,
input: [
{
type: "function_call_output",
call_id: call.call_id,
output: JSON.stringify(result),
},
{ role: "user", content: "Now optimize it." },
],
tools,
tool_choice: "none",
});
await waitForResponse(ws);
} finally {
ws.close();
}Cómo funciona la continuación
El modo WebSocket usa la misma semántica de encadenamiento de previous_response_id que el modo HTTP, pero agrega una vía de continuación de menor latencia en el socket activo.
En una conexión WebSocket activa, el servicio conserva el estado reciente de respuestas anteriores en una caché en memoria propia de la conexión. Cuando usas stream_id, cada carril conserva su última respuesta en caché, por lo que continuar desde la última respuesta de ese carril es rápido: el servicio puede reutilizar el estado local de la conexión. Como el servicio conserva el estado de respuestas anteriores solo en memoria y no lo escribe en disco, puedes usar el modo WebSocket de forma compatible con store=false y la retención cero de datos (ZDR).
Si un previous_response_id no está en la caché en memoria, el comportamiento depende de si almacenas las respuestas:
- Con
store=true, el servicio puede recuperar el estado correspondiente a ID de respuestas anteriores a partir del estado persistido, cuando esté disponible. La continuación puede seguir funcionando, pero pierde la ventaja de latencia que ofrece la caché en memoria. - Con
store=false(incluido ZDR), no hay un estado persistido al que recurrir. Si el ID no está en caché, la solicitud devuelveprevious_response_not_found.
Si una continuación en el mismo carril devuelve un 4xx o 5xx, el servicio elimina el previous_response_id referenciado de la caché local de la conexión. Un fork entre carriles que devuelve un error conserva la respuesta de origen compartida para que el carril de origen pueda continuar.
Compactación y creación de nuevas respuestas
Si usas compactación, hay dos patrones de continuación diferentes:
Compactación del lado del servidor (context_management)
Cuando habilitas la compactación del lado del servidor (context_management con compact_threshold), la compactación ocurre durante la generación normal de /responses. En el modo WebSocket, continúas como de costumbre: envía el siguiente response.create con el último previous_response_id y solo los nuevos elementos de entrada.
Uso independiente de /responses/compact
El punto de acceso /responses/compact independiente devuelve una nueva ventana de entrada compactada, no un ID de respuesta. Después de la compactación, crea una nueva respuesta en tu conexión WebSocket usando la ventana compactada como input (junto con los siguientes elementos del usuario o de las herramientas).
Inicia una nueva cadena omitiendo previous_response_id o estableciéndolo en null. Pasa la salida compactada tal como se devuelve; no elimines elementos de la ventana devuelta.
import { toResponseInputItems } from "openai/lib/responses/ResponseInputItems";
// Compact your current window with an HTTP request.
const compacted = await client.responses.compact({
model: "gpt-6-astra",
input: longInputItems,
});
const nextInput = toResponseInputItems(compacted.output);
nextInput.push({
type: "message",
role: "user",
content: [{ type: "input_text", text: "Continue from here." }],
});
// Start a new response on the WebSocket using the compacted window.
const ws = new ResponsesWS(client);
try {
ws.send({
type: "response.create",
stream_id: "main",
model: "gpt-6-astra",
store: false,
input: nextInput,
tools: [],
});
let completed = false;
for await (const event of ws) {
if (event.type === "error") throw event.error;
if (event.type !== "message") continue;
const message = event.message;
if (message.type === "response.output_text.delta") {
process.stdout.write(message.delta);
} else if (message.type === "response.completed") {
completed = true;
break;
} else if (
message.type === "response.failed" ||
message.type === "response.incomplete"
) {
throw new Error(JSON.stringify(message));
}
}
if (!completed)
throw new Error("Connection closed before the response finished.");
} finally {
ws.close();
}Ejecutar conversaciones en paralelo
Puedes mantener conversaciones en paralelo en la misma conexión usando el parámetro stream_id. Envía eventos response.create independientes uno tras otro con diferentes valores de stream_id. El servidor puede ejecutarlos de forma concurrente en una sola conexión. Sus eventos pueden intercalarse, así que mantén un único bucle de lectura y dirige cada evento según su stream_id.
Un stream_id da nombre a un carril ordenado en una conexión WebSocket. Mantén separados stream_id y previous_response_id:
stream_idcontrola a dónde se dirigen los eventos y qué solicitudes se ejecutan en orden de llegada.previous_response_idcontrola el linaje de la conversación.
Esa separación permite usar dos patrones útiles.
one WebSocket connection
├─ stream_id="planner" draft a deployment plan
└─ stream_id="research" list deployment risks
Las solicitudes con el mismo stream_id mantienen el orden de llegada y no se superponen. Las solicitudes con diferentes valores de stream_id pueden ejecutarse de forma concurrente.
Límites por conexión
- Una conexión puede tener hasta 16 respuestas activas en curso entre los carriles con nombre y el predeterminado. La conexión acepta más eventos
response.createy los pone en cola hasta que termine una respuesta activa. - Una conexión acepta hasta 32 valores distintos de
stream_idpara flujos con nombre. El carril predeterminado implícito no cuenta para este límite de flujos con nombre. Reutiliza unstream_idexistente o abre una nueva conexión al alcanzar el límite.
Crear un fork de una conversación en un nuevo flujo
Para crear una rama a partir de una respuesta completada, envía su ID como previous_response_id con un nuevo stream_id. Mientras esa respuesta siga disponible, el nuevo flujo hereda su contexto y el flujo original puede continuar. Una vez que se inicia el fork, ambas ramas pueden ejecutarse de forma concurrente porque usan distintos ID de flujo.
Con store=false (incluido ZDR), un fork entre carriles depende de que la respuesta de origen permanezca en la caché local de la conexión. Si el fork queda en cola mientras el carril de origen avanza o falla, la respuesta de origen puede eliminarse de la caché antes de que se inicie el fork, y este devuelve previous_response_not_found. Espera a que el carril del fork emita response.in_progress antes de hacer avanzar el carril de origen, o reintenta con previous_response_id establecido en null y vuelve a enviar todo el contexto de entrada.
main: resp_1 ──▶ resp_2 ──▶ resp_3
╲
critic: resp_4 ──▶ resp_5
Reutilizar un stream_id sin previous_response_id inicia una nueva respuesta; no continúa la conversación.
Las llamadas principales son así:
# One socket, two independent conversations.
send_create(connection, "planner", "Draft a deployment plan.")
send_create(connection, "research", "List deployment risks.")
# Fork the planner response, then continue the original branch in parallel.
send_create(
connection,
"critic",
"Find gaps in this plan.",
previous_response_id=planner_response_id,
)
wait_for_in_progress(connection, "critic")
send_create(
connection,
"planner",
"Add rollback steps.",
previous_response_id=planner_response_id,
)
Ejemplo completo
import OpenAI from "openai";
import { ResponsesWS } from "openai/resources/responses/ws";
const client = new OpenAI();
const latestResponseIdByLane = new Map();
function sendCreate(
ws,
streamId,
text,
previousResponseId = latestResponseIdByLane.get(streamId)
) {
ws.send({
type: "response.create",
stream_id: streamId,
model: "gpt-6-astra",
store: false,
input: [
{
type: "message",
role: "user",
content: [{ type: "input_text", text }],
},
],
previous_response_id: previousResponseId,
});
}
async function readMessage(events) {
while (true) {
const { value: event, done } = await events.next();
if (done)
throw new Error("Connection closed before all responses finished.");
if (event.type === "error") throw event.error;
if (event.type !== "message") continue;
const message = event.message;
if (
message.type === "response.failed" ||
message.type === "response.incomplete"
) {
throw new Error(
`Lane ${message.stream_id} failed: ${JSON.stringify(message)}`
);
}
return message;
}
}
async function drainUntilComplete(events, expectedStreamIds) {
const remaining = new Set(expectedStreamIds);
while (remaining.size > 0) {
const message = await readMessage(events);
const streamId = message.stream_id;
if (!streamId || !remaining.has(streamId)) continue;
if (message.type === "response.completed") {
latestResponseIdByLane.set(streamId, message.response.id);
remaining.delete(streamId);
}
}
}
async function waitForInProgress(events, streamId) {
while (true) {
const message = await readMessage(events);
if (
message.type === "response.in_progress" &&
message.stream_id === streamId
)
return;
}
}
const ws = new ResponsesWS(client);
// Keep one iterator so events stay queued while moving between phases.
const events = ws.stream();
try {
// Run two independent conversations in parallel.
sendCreate(
ws,
"planner",
"Draft a deployment plan for a stateless API service."
);
sendCreate(
ws,
"research",
"List common deployment risks for a stateless API service."
);
await drainUntilComplete(events, new Set(["planner", "research"]));
// Fork the planner conversation and continue its original branch in parallel.
const plannerResponseId = latestResponseIdByLane.get("planner");
sendCreate(
ws,
"critic",
"Find gaps in this deployment plan.",
plannerResponseId
);
// Let the fork load its parent before advancing the original lane's cache.
await waitForInProgress(events, "critic");
sendCreate(
ws,
"planner",
"Add rollback and monitoring steps to the plan.",
plannerResponseId
);
await drainUntilComplete(events, new Set(["critic", "planner"]));
} finally {
await events.return?.();
ws.close();
}Un stream_id debe tener entre 1 y 256 caracteres y solo puede contener letras, números, guiones bajos (_), guiones (-) y puntos (.). Úsalo solo en eventos response.create de WebSocket; no lo incluyas en POST /v1/responses por HTTP.
Para los flujos con nombre, los eventos del servidor incluyen el stream_id correspondiente, incluidos los eventos de finalización y los errores específicos de la solicitud.
Si omites stream_id, la solicitud usa un carril predeterminado implícito y sus eventos no incluyen stream_id. Por lo demás, el carril predeterminado sigue las mismas reglas de orden y concurrencia que los flujos con nombre. Una cadena vacía no es un stream_id válido; omite el campo para seleccionar el carril predeterminado.
Comportamiento y límites de la conexión
- Los eventos de cada respuesta siguen el modelo de eventos de streaming existente de Responses. Los eventos de distintos carriles pueden intercalarse.
- Las solicitudes con el mismo
stream_idse ejecutan en orden de llegada y no se superponen. Las solicitudes en distintos carriles pueden ejecutarse de forma concurrente. - Las conexiones duran hasta 60 minutos. Vuelve a conectarte al alcanzar el límite.
Reconexión y recuperación
Cuando una conexión se cierra (o alcanza el límite de 60 minutos), su caché local desaparece para todos los carriles. Abre una nueva conexión WebSocket y recupera cada carril con uno de estos métodos:
- Si almacenaste una respuesta anterior (
store=true) y tienes un ID de respuesta válido, continúa ese carril conprevious_response_idy nuevos elementos de entrada. - Si no puedes continuar un carril (por ejemplo, con
store=false/ZDR o anteprevious_response_not_found), inicia una nueva respuesta estableciendoprevious_response_idennull(u omitiéndolo) y envía el contexto de entrada completo para el siguiente turno de ese carril. - Si compactaste el contexto con
/responses/compact, usa la ventana compactada devuelta comoinputbase para esa nueva respuesta y luego agrega los elementos más recientes del usuario o de las herramientas.
Errores que debes manejar
Cuando el servidor puede asociar un error con un carril con nombre, el evento de error incluye stream_id. Los demás carriles pueden continuar después de un error limitado a una solicitud.
previous_response_not_found
{
"type": "error",
"status": 400,
"stream_id": "main",
"error": {
"type": "invalid_request_error",
"code": "previous_response_not_found",
"message": "Previous response with id 'resp_abc' not found.",
"param": "previous_response_id"
}
}
invalid_stream_id
{
"type": "error",
"status": 400,
"error": {
"type": "invalid_request_error",
"code": "invalid_stream_id",
"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.",
"param": "stream_id"
}
}
websocket_stream_limit_reached
{
"type": "error",
"status": 400,
"stream_id": "agent_33",
"error": {
"type": "invalid_request_error",
"code": "websocket_stream_limit_reached",
"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.",
"param": "stream_id"
}
}
websocket_connection_limit_reached
{
"type": "error",
"error": {
"type": "invalid_request_error",
"code": "websocket_connection_limit_reached",
"message": "Responses websocket connection limit reached (60 minutes). Create a new websocket connection to continue."
},
"status": 400
}