Die Responses API unterstützt einen WebSocket-Modus für lang andauernde Arbeitsabläufe mit vielen Tool-Aufrufen. Neben einer geringeren Latenz ermöglicht stream_id WebSocket-Multiplexing: Über eine persistente Verbindung zu /v1/responses kannst du parallele Konversationen führen und eine bestehende Konversation auf einen neuen Stream forken. Sende zum Fortsetzen in jedem Schritt nur neue Eingabeelemente zusammen mit previous_response_id.
Der WebSocket-Modus ist sowohl mit der Option „keine Datenaufbewahrung“ (ZDR) als auch mit store=false kompatibel.
Warum den WebSocket-Modus verwenden?
Der WebSocket-Modus ist besonders nützlich, wenn ein Arbeitsablauf viele Anfrage-Antwort-Zyklen zwischen Modell und Tools umfasst, beispielsweise beim agentischen Programmieren oder in Orchestrierungsschleifen mit wiederholten Tool-Aufrufen.
Da die Verbindung offen bleibt und in jedem Schritt nur inkrementelle Eingaben gesendet werden, verringert der WebSocket-Modus den Aufwand für jede Fortsetzung und senkt die Ende-zu-Ende-Latenz bei langen Ketten. Bei Durchläufen mit mindestens 20 Tool-Aufrufen haben wir eine um bis zu etwa 40 % schnellere Ende-zu-Ende-Ausführung beobachtet.
Verbindung herstellen und Antworten erstellen
Installiere die WebSocket-Abhängigkeiten mit pip install "openai[realtime]>=3.8.0" für Python, npm install openai@^7.10.0 ws für JavaScript oder gem install openai async-websocket für Ruby.
Im WebSocket-Modus beginnst du jeden Schritt, indem du vom Client ein response.create-Ereignis sendest. Die Nutzlast entspricht dem üblichen Anfrage-Body zum Erstellen einer Antwort, allerdings werden transportspezifische Felder wie stream und background nicht verwendet.
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();
}Clients können den Anfragezustand optional vorbereiten, indem sie response.create mit generate: false senden. Das ist nützlich, wenn du bereits weißt, welche Tools, Anweisungen und/oder benutzerdefinierten Nachrichten du in einem kommenden Schritt senden möchtest. generate: false liefert keine Modellausgabe, sondern bereitet den Anfragezustand vor, damit die nächste Generierung schneller beginnen kann. Die vorbereitende Anfrage gibt eine Antwort-ID zurück, an die du mit previous_response_id anknüpfen kannst, auch in späteren Schritten einer Antwortkette. Im nächsten Abschnitt erfährst du, wie du eine Sitzung mit previous_response_id und inkrementellen Eingaben fortsetzt.
Mit inkrementellen Eingaben fortsetzen
Um während einer laufenden Antwort weitere Nutzeranweisungen hinzuzufügen, nutze Während der Ausführung nachsteuern. Beim Nachsteuern bleiben abgeschlossene Arbeiten erhalten, und die neuen Anweisungen werden bei der Fortsetzung berücksichtigt. Verwende das folgende Muster mit response.create für die gewöhnliche Fortsetzung zwischen Schritten und für Tool-Ergebnisse.
Um einen Durchlauf fortzusetzen, sende ein weiteres response.create-Ereignis mit diesen Angaben:
- Setze
previous_response_idauf die ID der vorherigen Antwort. - Übergib in
inputnur neue Elemente, beispielsweise Tool-Ausgaben und die nächste Nutzernachricht.
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();
}So funktioniert die Fortsetzung
Im WebSocket-Modus funktioniert die Verkettung über previous_response_id genauso wie im HTTP-Modus. Zusätzlich ermöglicht er eine Fortsetzung mit geringerer Latenz über den aktiven Socket.
Bei einer aktiven WebSocket-Verbindung hält der Dienst den Zustand kürzlich erstellter Antworten in einem verbindungslokalen Arbeitsspeicher-Cache vor. Wenn du stream_id verwendest, behält jede Verarbeitungsspur ihre zuletzt zwischengespeicherte Antwort. Dadurch lässt sich die Konversation von der neuesten Antwort dieser Spur aus schnell fortsetzen, da der Dienst den verbindungslokalen Zustand wiederverwenden kann. Der Dienst hält den Zustand vorheriger Antworten nur im Arbeitsspeicher vor und schreibt ihn nicht auf die Festplatte. Daher kannst du den WebSocket-Modus so nutzen, dass er mit store=false und der Option „keine Datenaufbewahrung“ (ZDR) kompatibel ist.
Wenn eine previous_response_id nicht im Arbeitsspeicher-Cache vorhanden ist, hängt das Verhalten davon ab, ob du Antworten speicherst:
- Bei
store=truekann der Dienst den Zustand älterer Antwort-IDs aus dauerhaft gespeicherten Daten wiederherstellen, sofern diese verfügbar sind. Die Fortsetzung kann weiterhin funktionieren, verliert aber den Latenzvorteil des Arbeitsspeicher-Caches. - Bei
store=false(einschließlich ZDR) gibt es keine dauerhaft gespeicherten Daten, auf die der Dienst zurückgreifen kann. Wenn die ID nicht im Cache vorhanden ist, gibt die Anfrageprevious_response_not_foundzurück.
Wenn eine Fortsetzung innerhalb derselben Spur einen 4xx- oder 5xx-Fehler zurückgibt, entfernt der Dienst die referenzierte previous_response_id aus dem verbindungslokalen Cache. Gibt ein Fork auf eine andere Spur einen Fehler zurück, bleibt die gemeinsame Ausgangsantwort erhalten, sodass die ursprüngliche Spur fortgesetzt werden kann.
Compaction (Kontextverdichtung) und Erstellen neuer Antworten
Wenn du Compaction (Kontextverdichtung) nutzt, gibt es zwei unterschiedliche Vorgehensweisen für die Fortsetzung:
Serverseitige Compaction (Kontextverdichtung) (context_management)
Wenn du die serverseitige Compaction (Kontextverdichtung) aktivierst (context_management mit compact_threshold), wird der Kontext während der normalen Generierung über /responses verdichtet. Im WebSocket-Modus setzt du die Konversation wie gewohnt fort: Sende das nächste response.create-Ereignis mit der neuesten previous_response_id und ausschließlich neuen Eingabeelementen.
Separater Aufruf von /responses/compact
Der separate Endpunkt /responses/compact gibt ein neues, verdichtetes Eingabefenster zurück, keine Antwort-ID. Erstelle nach der Compaction (Kontextverdichtung) eine neue Antwort über deine WebSocket-Verbindung. Verwende dabei das verdichtete Fenster als input und ergänze die nächsten Nutzer- oder Tool-Elemente.
Beginne eine neue Kette, indem du previous_response_id weglässt oder auf null setzt. Übergib die verdichtete Ausgabe unverändert. Kürze das zurückgegebene Fenster nicht.
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();
}Konversationen parallel führen
Mit dem Parameter stream_id kannst du parallele Konversationen über dieselbe Verbindung führen. Sende unabhängige response.create-Ereignisse mit unterschiedlichen Werten für stream_id direkt nacheinander. Der Server kann sie über eine Verbindung gleichzeitig ausführen. Ihre Ereignisse können verschachtelt eintreffen. Verwende daher eine einzige Leseschleife und leite jedes Ereignis anhand von stream_id weiter.
Eine stream_id bezeichnet eine Verarbeitungsspur mit festgelegter Reihenfolge innerhalb einer WebSocket-Verbindung. Unterscheide zwischen stream_id und previous_response_id:
stream_idsteuert, wohin Ereignisse geleitet werden und welche Anfragen nach dem First-in-first-out-Prinzip ausgeführt werden.previous_response_idsteuert, an welche vorherige Antwort die Konversation anknüpft.
Diese Trennung ermöglicht zwei nützliche Vorgehensweisen.
one WebSocket connection
├─ stream_id="planner" draft a deployment plan
└─ stream_id="research" list deployment risks
Anfragen mit derselben stream_id werden weiterhin nach dem First-in-first-out-Prinzip ausgeführt und überlappen sich nicht. Anfragen mit unterschiedlichen Werten für stream_id können gleichzeitig ausgeführt werden.
Limits pro Verbindung
- Über eine Verbindung können auf den benannten Spuren und der Standardspur insgesamt bis zu 16 Antworten gleichzeitig in Bearbeitung sein. Die Verbindung nimmt weitere
response.create-Ereignisse an und stellt sie in eine Warteschlange, bis eine aktive Antwort abgeschlossen ist. - Eine Verbindung akzeptiert bis zu 32 unterschiedliche Werte für
stream_idzur Benennung von Streams. Die implizite Standardspur zählt nicht zu diesem Limit für benannte Streams. Verwende nach Erreichen des Limits eine vorhandenestream_iderneut oder öffne eine neue Verbindung.
Eine Konversation auf einen neuen Stream forken
Um von einer abgeschlossenen Antwort abzuzweigen, sende ihre ID als previous_response_id zusammen mit einer neuen stream_id. Solange diese Antwort verfügbar bleibt, übernimmt der neue Stream ihren Kontext, und der ursprüngliche Stream kann weiterlaufen. Sobald der Fork startet, können beide Zweige gleichzeitig laufen, da sie unterschiedliche Stream-IDs verwenden.
Bei store=false (einschließlich ZDR) setzt ein Fork auf eine andere Spur voraus, dass die Ausgangsantwort im verbindungslokalen Cache verbleibt. Wartet der Fork in der Warteschlange, während die ursprüngliche Spur fortgesetzt wird oder fehlschlägt, kann die Ausgangsantwort vor dem Start des Forks aus dem Cache entfernt werden. Der Fork gibt dann previous_response_not_found zurück. Warte, bis die neue Spur response.in_progress ausgibt, bevor du die ursprüngliche Spur fortsetzt. Alternativ kannst du es erneut versuchen, indem du previous_response_id auf null setzt und den vollständigen Eingabekontext erneut sendest.
main: resp_1 ──▶ resp_2 ──▶ resp_3
╲
critic: resp_4 ──▶ resp_5
Wenn du eine stream_id ohne previous_response_id wiederverwendest, beginnt eine neue Antwort. Die Konversation wird dadurch nicht fortgesetzt.
Die wichtigsten Aufrufe sehen so aus:
# 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,
)
Vollständiges Beispiel
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();
}Eine stream_id muss 1–256 Zeichen lang sein und darf nur Buchstaben, Ziffern, Unterstriche (_), Bindestriche (-) und Punkte (.) enthalten. Verwende sie ausschließlich in response.create-Ereignissen über WebSocket. Füge sie nicht in HTTP-Anfragen mit POST /v1/responses ein.
Bei benannten Streams enthalten Serverereignisse die zugehörige stream_id. Das gilt auch für abschließende Ereignisse und anfragebezogene Fehler.
Wenn du stream_id weglässt, verwendet die Anfrage eine implizite Standardspur, und ihre Ereignisse enthalten keine stream_id. Ansonsten gelten für die Standardspur dieselben Regeln für Reihenfolge und gleichzeitige Ausführung wie für benannte Streams. Eine leere Zeichenfolge ist kein gültiger Wert für stream_id. Lass das Feld weg, um die Standardspur auszuwählen.
Verbindungsverhalten und Limits
- Ereignisse innerhalb einer Antwort folgen dem bestehenden Modell für Streaming-Ereignisse der Responses API. Ereignisse aus unterschiedlichen Spuren können verschachtelt eintreffen.
- Anfragen mit derselben
stream_idwerden nach dem First-in-first-out-Prinzip ausgeführt und überlappen sich nicht. Anfragen auf unterschiedlichen Spuren können gleichzeitig ausgeführt werden. - Verbindungen bleiben bis zu 60 Minuten bestehen. Stelle beim Erreichen dieses Limits eine neue Verbindung her.
Verbindung erneut herstellen und Zustand wiederherstellen
Wenn eine Verbindung geschlossen wird (oder das Limit von 60 Minuten erreicht), geht ihr verbindungslokaler Cache für alle Spuren verloren. Öffne eine neue WebSocket-Verbindung und stelle jede Spur mit einem der folgenden Verfahren wieder her:
- Wenn du eine frühere Antwort gespeichert hast (
store=true) und eine gültige Antwort-ID hast, setze diese Spur mitprevious_response_idund neuen Eingabeelementen fort. - Wenn du eine Spur nicht fortsetzen kannst (etwa bei
store=false/ZDR oderprevious_response_not_found), starte eine neue Antwort, indem duprevious_response_idaufnullsetzt (oder den Parameter weglässt). Sende dabei den vollständigen Eingabekontext für den nächsten Gesprächsschritt dieser Spur. - Wenn du den Kontext mit
/responses/compactverdichtet hast, verwende das zurückgegebene verdichtete Kontextfenster als Grundlage fürinputder neuen Antwort. Hänge anschließend die neuesten Nutzer- und Tool-Elemente an.
Fehler, die du behandeln solltest
Wenn der Server einen Fehler einer benannten Spur zuordnen kann, enthält das Fehlerereignis stream_id. Andere Spuren können nach einem Fehler, der nur eine einzelne Anfrage betrifft, weiterlaufen.
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
}