A Responses API oferece um modo WebSocket para fluxos de trabalho de longa duração com muitas chamadas de ferramentas. Além de reduzir a latência, stream_id permite a multiplexação WebSocket: uma única conexão persistente com /v1/responses pode executar conversas paralelas e criar um fork de uma conversa existente em um novo fluxo. Continue cada turno enviando apenas novos itens de entrada junto com previous_response_id.
O modo WebSocket é compatível tanto com zero retenção de dados (ZDR) quanto com store=false.
Por que usar o modo WebSocket
O modo WebSocket é especialmente útil quando um fluxo de trabalho envolve muitas trocas entre o modelo e as ferramentas (por exemplo, programação agêntica ou ciclos de orquestração com chamadas repetidas de ferramentas).
Como a conexão permanece aberta e cada turno envia apenas entradas incrementais, o modo WebSocket reduz a sobrecarga de continuação por turno e melhora a latência de ponta a ponta em cadeias longas. Em execuções com 20 ou mais chamadas de ferramentas, observamos uma execução de ponta a ponta até cerca de 40% mais rápida.
Conecte-se e crie respostas
Instale as dependências do WebSocket com pip install "openai[realtime]>=3.8.0" para Python, npm install openai@^7.10.0 ws para JavaScript ou gem install openai async-websocket para Ruby.
No modo WebSocket, inicie cada turno enviando um evento response.create a partir do cliente. O payload segue o corpo padrão de criação de respostas da Responses API, exceto pelos campos específicos do transporte, como stream e background, que não são usados.
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();
}Os clientes podem, opcionalmente, preparar o estado da requisição com antecedência enviando response.create com generate: false. Isso é útil quando você já sabe quais ferramentas, instruções e/ou mensagens personalizadas pretende enviar em um próximo turno. generate: false não retorna uma saída do modelo, mas prepara o estado da requisição para que a geração do próximo turno possa começar mais rápido. A requisição de preparação retorna um ID de resposta que você pode usar como ponto de partida para um encadeamento com previous_response_id, inclusive em turnos posteriores de uma cadeia de respostas. A próxima seção explica como continuar uma sessão usando previous_response_id e entradas incrementais.
Continue com entradas incrementais
Para adicionar instruções do usuário enquanto uma resposta ainda está em andamento, use a Orientação durante o turno. A orientação preserva o trabalho concluído e inclui as novas instruções em uma continuação. Use o padrão de response.create a seguir para a continuação habitual entre turnos e para resultados de ferramentas.
Para continuar uma execução, envie outro response.create com:
previous_response_iddefinido como o ID da resposta anterior.inputcontendo apenas novos itens (por exemplo, saídas de ferramentas e a próxima mensagem do usuário).
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();
}Como funciona a continuação
O modo WebSocket usa a mesma semântica de encadeamento com previous_response_id do modo HTTP, mas acrescenta um caminho de continuação com menor latência no socket ativo.
Em uma conexão WebSocket ativa, o serviço mantém o estado recente de respostas anteriores em um cache em memória, local à conexão. Quando você usa stream_id, cada faixa mantém sua resposta mais recente em cache. Assim, continuar a partir da última resposta dessa faixa é rápido, pois o serviço pode reutilizar o estado local à conexão. Como o serviço retém o estado das respostas anteriores apenas na memória e não o grava em disco, você pode usar o modo WebSocket de forma compatível com store=false e zero retenção de dados (ZDR).
Se um previous_response_id não estiver no cache em memória, o comportamento dependerá de você armazenar ou não as respostas:
- Com
store=true, o serviço pode restaurar o estado de IDs de respostas mais antigas a partir do estado persistido, quando disponível. A continuação ainda pode funcionar, mas perde o benefício de latência do cache em memória. - Com
store=false(incluindo ZDR), não há estado persistido ao qual recorrer como alternativa. Se o ID não estiver em cache, a requisição retornaráprevious_response_not_found.
Se uma continuação na mesma faixa retornar 4xx ou 5xx, o serviço removerá o previous_response_id referenciado do cache local à conexão. Um fork entre faixas que retorna um erro preserva a resposta de origem compartilhada para que a faixa de origem possa continuar.
Compactação e criação de novas respostas
Se você estiver usando compactação, há dois padrões diferentes de continuação:
Compactação no servidor (context_management)
Quando você ativa a compactação no servidor (context_management com compact_threshold), ela ocorre durante a geração normal em /responses. No modo WebSocket, a continuação funciona da maneira habitual: envie o próximo response.create com o previous_response_id mais recente e apenas novos itens de entrada.
Uso independente de /responses/compact
O endpoint /responses/compact independente retorna uma nova janela de entrada compactada, não um ID de resposta. Após a compactação, crie uma nova resposta na sua conexão WebSocket usando a janela compactada como input (junto com os próximos itens do usuário ou de ferramentas).
Inicie uma nova cadeia omitindo previous_response_id ou definindo-o como null. Passe a saída compactada sem alterações; não remova itens da janela retornada.
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();
}Execute conversas em paralelo
Você pode manter conversas paralelas na mesma conexão usando o parâmetro stream_id. Envie eventos response.create independentes em sequência, com valores diferentes de stream_id. O servidor pode executá-los simultaneamente em uma única conexão. Os eventos podem se intercalar, portanto mantenha um único loop de leitura e encaminhe cada evento de acordo com stream_id.
Um stream_id nomeia uma faixa ordenada em uma conexão WebSocket. Mantenha stream_id e previous_response_id separados:
stream_idcontrola o destino dos eventos e quais requisições são executadas na ordem de chegada.previous_response_idcontrola a linhagem da conversa.
Essa separação permite dois padrões úteis.
one WebSocket connection
├─ stream_id="planner" draft a deployment plan
└─ stream_id="research" list deployment risks
Requisições com o mesmo stream_id seguem a ordem de chegada e não se sobrepõem. Requisições com valores diferentes de stream_id podem ser executadas simultaneamente.
Limites por conexão
- Uma conexão pode ter até 16 respostas ativas em andamento entre as faixas nomeadas e a faixa padrão. A conexão aceita mais eventos
response.createe os coloca em fila até que uma resposta ativa termine. - Uma conexão aceita até 32 valores distintos de
stream_idpara fluxos nomeados. A faixa padrão implícita não conta para esse limite de fluxos nomeados. Reutilize umstream_idexistente ou abra uma nova conexão ao atingir o limite.
Crie um fork de uma conversa em um novo fluxo
Para criar uma ramificação a partir de uma resposta concluída, envie o ID dela como previous_response_id com um novo stream_id. Enquanto essa resposta permanecer disponível, o novo fluxo herdará seu contexto, e o fluxo original poderá continuar. Depois que o fork começar, ambas as ramificações poderão ser executadas simultaneamente, pois usam IDs de fluxo diferentes.
Com store=false (incluindo ZDR), um fork entre faixas depende de a resposta de origem permanecer no cache local à conexão. Se o fork entrar na fila enquanto a faixa de origem avança ou falha, a resposta de origem poderá ser removida do cache antes que o fork comece, e o fork retornará previous_response_not_found. Espere a faixa do fork emitir response.in_progress antes de avançar a faixa de origem, ou tente novamente com previous_response_id definido como null e reenvie todo o contexto de entrada.
main: resp_1 ──▶ resp_2 ──▶ resp_3
╲
critic: resp_4 ──▶ resp_5
Reutilizar um stream_id sem previous_response_id inicia uma nova resposta; isso não dá continuidade à conversa.
As principais chamadas são assim:
# 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,
)
Exemplo 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();
}Um stream_id deve ter de 1 a 256 caracteres e pode conter apenas letras, números, sublinhados (_), hífens (-) e pontos (.). Use-o apenas em eventos response.create via WebSocket; não o inclua em requisições HTTP POST /v1/responses.
Para fluxos nomeados, os eventos do servidor incluem o stream_id correspondente, inclusive eventos de término e erros restritos à requisição.
Se você omitir stream_id, a requisição usará uma faixa padrão implícita, e seus eventos não incluirão stream_id. Nos demais aspectos, a faixa padrão segue as mesmas regras de ordenação e concorrência dos fluxos nomeados. Uma string vazia não é um stream_id válido; omita o campo para selecionar a faixa padrão.
Comportamento e limites da conexão
- Os eventos de cada resposta seguem o modelo existente de eventos de streaming da Responses API. Eventos de faixas diferentes podem se intercalar.
- Requisições com o mesmo
stream_idsão executadas na ordem de chegada e não se sobrepõem. Requisições em faixas diferentes podem ser executadas simultaneamente. - As conexões duram até 60 minutos. Reconecte-se ao atingir esse limite.
Reconecte-se e recupere o estado
Quando uma conexão é encerrada (ou atinge o limite de 60 minutos), o cache local da conexão é perdido para todas as vias. Abra uma nova conexão WebSocket e recupere cada via com uma destas abordagens:
- Se você armazenou uma resposta anterior (
store=true) e tem um ID de resposta válido, continue essa via comprevious_response_ide novos itens de entrada. - Se não for possível continuar uma via (por exemplo,
store=false/ZDR ouprevious_response_not_found), inicie uma nova resposta definindoprevious_response_idcomonull(ou omitindo esse parâmetro) e envie o contexto de entrada completo para o próximo turno dessa via. - Se você compactou o contexto com
/responses/compact, use a janela compactada retornada como base para oinputdessa nova resposta e, em seguida, acrescente os itens mais recentes do usuário e das ferramentas.
Erros a tratar
Quando o servidor consegue associar um erro a uma via nomeada, o evento de erro inclui stream_id. As outras vias podem continuar após um erro restrito a uma requisição.
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
}