Responses API 支援 WebSocket 模式,適用於長時間執行且頻繁呼叫工具的工作流程。除了降低延遲,stream_id 還能啟用 WebSocket 多工處理:只要透過單一持續連至 /v1/responses 的連線,就能同時執行多個對話,並將現有對話分支到新的串流。每個回合只需傳送新的輸入項目及 previous_response_id,即可延續執行。
WebSocket 模式同時相容於零資料保留(ZDR)和 store=false。
為什麼要使用 WebSocket 模式
當工作流程涉及模型與工具之間的多次往返互動時,WebSocket 模式最能發揮效益,例如智慧體式程式碼編寫,或反覆呼叫工具的編排迴圈。
由於連線持續開啟,且每個回合只傳送增量輸入,WebSocket 模式能減少延續每個回合所需的額外負擔,並降低長回應鏈的端到端延遲。在包含 20 次以上工具呼叫的執行過程中,我們觀察到端到端執行速度最高可提升約 40%。
建立連線及回應
請依使用的語言安裝 WebSocket 相依套件:Python 使用 pip install "openai[realtime]>=3.8.0",JavaScript 使用 npm install openai@^7.10.0 ws,Ruby 則使用 gem install openai async-websocket。
在 WebSocket 模式中,請從用戶端傳送 response.create 事件來開始每個回合。承載內容與一般的 Responses 建立請求主體相同,但不使用 stream 和 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();
}用戶端可以選擇傳送帶有 generate: false 的 response.create,預先準備請求狀態。當你已經知道下一個回合要傳送哪些工具、指令及/或自訂訊息時,這個做法很有用。generate: false 不會傳回模型輸出,而是準備好請求狀態,讓下一個生成回合能更快開始。預熱請求會傳回回應 ID,你可以透過 previous_response_id 從該回應接續回應鏈,也可以在回應鏈的後續回合中使用。下一節將說明如何使用 previous_response_id 和增量輸入延續工作階段。
使用增量輸入延續執行
若要在回應仍在執行時加入使用者指令,請使用回合中調整方向。調整方向會保留已完成的工作,並在接續執行時納入新指令。一般的回合間延續及工具結果傳送,則使用下列 response.create 模式。
若要延續執行,請再傳送一個 response.create,並設定:
- 將
previous_response_id設為先前的回應 ID。 - 讓
input僅包含新項目,例如工具輸出和下一則使用者訊息。
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();
}延續執行的運作方式
WebSocket 模式與 HTTP 模式使用相同的 previous_response_id 串接語意,但會在作用中的通訊端上提供延遲更低的延續執行途徑。
在作用中的 WebSocket 連線上,服務會將近期的先前回應狀態保留在該連線專屬的記憶體快取中。使用 stream_id 時,每個通道會保留其最新的快取回應,因此從該通道的最新回應接續執行時,服務可以重複使用連線專屬的狀態,加快執行速度。由於服務只將先前回應狀態保留在記憶體中,不會寫入磁碟,因此你可以用相容於 store=false 和零資料保留(ZDR)的方式使用 WebSocket 模式。
如果記憶體快取中沒有某個 previous_response_id,後續行為取決於你是否儲存回應:
- 使用
store=true時,若有可用的持久化狀態,服務可能會從中還原較舊回應 ID 的狀態。此時仍可延續執行,但無法享有記憶體快取帶來的低延遲優勢。 - 使用
store=false時(包括 ZDR),沒有持久化狀態可供備援。如果 ID 不在快取中,請求會傳回previous_response_not_found。
如果同一通道的延續請求傳回 4xx 或 5xx,服務會從連線專屬快取中移除所參照的 previous_response_id。跨通道分支若傳回錯誤,則會保留共用的父回應,讓來源通道可以繼續執行。
壓縮與建立新回應
如果你使用壓縮,有兩種不同的延續執行模式:
伺服器端壓縮(context_management)
啟用伺服器端壓縮(context_management 搭配 compact_threshold)後,壓縮會在一般的 /responses 生成過程中進行。在 WebSocket 模式中,延續執行的方式和平常相同:傳送下一個 response.create,並帶上最新的 previous_response_id,以及僅包含新項目的輸入。
獨立的 /responses/compact
獨立的 /responses/compact 端點會傳回新的壓縮輸入視窗,而非回應 ID。壓縮完成後,請將壓縮後的視窗作為 input,加上接下來的使用者/工具項目,在 WebSocket 連線上建立新回應。
省略 previous_response_id 或將其設為 null,即可開始新的回應鏈。請原樣傳入壓縮後的輸出,不要刪減傳回的視窗內容。
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();
}平行執行對話
你可以使用 stream_id 參數,在同一條連線上維持多個平行對話。請使用不同的 stream_id 值,接連傳送彼此獨立的 response.create 事件。伺服器可以在同一條連線上並行執行這些請求。各個請求的事件可能交錯出現,因此請只維持一個讀取迴圈,並依據 stream_id 將各事件分派到對應的處理流程。
stream_id 用來命名單一 WebSocket 連線上依序執行的通道。請區分 stream_id 和 previous_response_id 的用途:
stream_id控制事件的去向,以及哪些請求會依先進先出順序執行。previous_response_id控制對話的承接關係。
將兩者分開後,就能實現兩種實用的模式。
one WebSocket connection
├─ stream_id="planner" draft a deployment plan
└─ stream_id="research" list deployment risks
使用相同 stream_id 的請求會維持先進先出的順序,且執行時間不會重疊。使用不同 stream_id 值的請求則可以並行執行。
每條連線的限制
- 每條連線的具名通道與預設通道,合計最多可有 16 個正在處理中的回應。連線仍會接受更多
response.create事件,並將其排入佇列,直到有執行中的回應完成。 - 每條連線最多接受 32 個不同的具名
stream_id值。隱含的預設通道不計入這項具名串流限制。達到上限後,請重複使用現有的stream_id,或建立新連線。
將對話分支到新的串流
若要從已完成的回應建立分支,請將其 ID 作為 previous_response_id 傳送,並搭配新的 stream_id。只要該回應仍可用,新串流就會繼承其上下文,而原本的串流也能繼續執行。分支開始後,由於兩個分支使用不同的串流 ID,因此可以並行執行。
使用 store=false 時(包括 ZDR),跨通道分支需要父回應仍保留在連線專屬快取中。如果分支正在排隊,而來源通道繼續執行或失敗,父回應可能在分支開始前就被移除,導致分支傳回 previous_response_not_found。請等到分支通道發出 response.in_progress 後,再讓來源通道繼續執行;或將 previous_response_id 設為 null,重新傳送完整的輸入上下文來重試。
main: resp_1 ──▶ resp_2 ──▶ resp_3
╲
critic: resp_4 ──▶ resp_5
重複使用 stream_id 但未提供 previous_response_id 時,會開始新的回應,而不會延續對話。
關鍵呼叫如下:
# 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,
)
完整範例
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();
}stream_id 的長度必須為 1–256 個字元,且只能包含字母、數字、底線(_)、連字號(-)和句點(.)。請僅在 WebSocket 的 response.create 事件中使用此欄位,不要將其加入 HTTP POST /v1/responses。
對於具名串流,伺服器事件會包含對應的 stream_id,包括終止事件及個別請求的錯誤。
如果省略 stream_id,請求會使用隱含的預設通道,且其事件不會包含 stream_id。除此之外,預設通道遵循與具名串流相同的排序與並行規則。空字串不是有效的 stream_id;若要選擇預設通道,請省略此欄位。
連線行為與限制
- 每個回應內的事件都遵循現有的 Responses 串流事件模型。不同通道的事件可能交錯出現。
- 使用相同
stream_id的請求會依先進先出順序執行,且執行時間不會重疊。不同通道上的請求則可以並行執行。 - 連線最多可持續 60 分鐘。達到時限時,請重新連線。
重新連線與復原
連線關閉時(或達到 60 分鐘上限時),所有通道在該連線上的本機快取都會消失。請開啟新的 WebSocket 連線,並使用下列其中一種方式恢復各個通道:
- 如果你已儲存先前的回應(
store=true),且持有有效的回應 ID,請使用previous_response_id和新的輸入項目來延續該通道。 - 如果無法延續通道(例如使用
store=false/ZDR,或收到previous_response_not_found),請將previous_response_id設為null(或省略此參數)以開始新的回應,並傳送該通道下一輪所需的完整輸入上下文。 - 如果你已使用
/responses/compact壓縮上下文,請以傳回的壓縮後視窗作為新回應的基礎input,再附加最新的使用者/工具項目。
需要處理的錯誤
當伺服器能將錯誤對應至具名通道時,錯誤事件就會包含 stream_id。發生僅影響單一請求的錯誤後,其他通道仍可繼續執行。
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
}