ターン途中で追加指示を送ると、ユーザーはレスポンスの完了を待たずに要件を追加したり、方針を変更したりできます。
ターン途中の追加指示は、WebSocket 接続で Responses API の GPT-6 モデルファミリーを使用する場合に利用できます。GPT-5.6 以前のモデルは対応していません。
追加指示を送っても、アプリケーションに送信済みの出力は書き換わりません。実行済みの操作が取り消されたり、すでに実行中のツールがキャンセルされたりすることもありません。
Java のサンプルには Java 17 以降が必要です。接続の設定方法と通信の基本的な動作については、WebSocket モードを参照してください。各イベントの正確な定義については、Responses WebSocket イベントリファレンスを参照してください。
response.create でレスポンスの生成を開始します。そのレスポンスの response.created イベントを受信したら、同じ接続で response.steer を送信します。previous_response_id には、そのレスポンスの ID を指定します。
12345{
"type": "response.steer",
"previous_response_id": "resp_1",
"input": "Keep the scope small enough for one developer to finish in two weeks."
}
このイベントで指定できるフィールドは type、previous_response_id、input のみです。input には、文字列、またはサポート対象のコンテンツタイプを使用したユーザーメッセージを 1 件以上含む配列を指定します。
API は入力をキューに追加すると、response.steer.accepted を返します。
12345678{
"type": "response.steer.accepted",
"sequence_number": 4,
"steer": {
"id": "steer_0123456789abcdef0123456789abcdef",
"previous_response_id": "resp_1"
}
}
この通知は、入力がキューに追加されたことを示します。この時点でモデルが指示を反映したとは限りません。アプリケーションからのツールの実行結果または承認が必要な場合を除き、API は追加指示を反映した新しいレスポンスを自動的に作成します。
サーバーは、現在の出力項目の生成と、すでに実行中のホスト型ツールの処理を終えてから、後続のレスポンスを自動的に作成します。追加指示を反映したレスポンスを受け取るには、そのままイベントの受信を続けてください。response.create を再度送信しないでください。
追加指示によって元のレスポンスが中断されると、response.incomplete イベントで終了し、incomplete_details.reason: "steered" が設定されます。元のレスポンスが先に正常終了した場合は、完了ステータスが維持されます。その場合でも、追加指示を反映した後続のレスポンスが作成されることがあります。
自動的に作成される後続のレスポンスは、元のリクエストの設定を引き継ぎます。トークン数とツール呼び出し回数の上限は、レスポンスごとに個別に適用されます。
.NET SDK には Responses API 用の WebSocket クライアントがないため、このサンプルには C# SDK 版はありません。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84// Set OPENAI_API_KEY before running this example.
// Install the SDK and WebSocket transport: npm install openai ws
import OpenAI from "openai";
import { ResponsesWS } from "openai/resources/responses/ws";
const client = new OpenAI();
const ws = new ResponsesWS(client, {
handshakeTimeout: 10_000,
});
let initialResponseId = "";
let successorResponseId = "";
let timeout;
try {
const output = await new Promise((resolve, reject) => {
timeout = setTimeout(() => {
reject(new Error("Timed out waiting for the steered response."));
ws.close();
}, 120_000);
ws.once("error", reject);
ws.once("close", () => {
reject(
new Error("Connection closed before the steered response finished.")
);
});
ws.on("event", (event) => {
try {
if (event.type === "response.created") {
if (!initialResponseId) {
initialResponseId = event.response.id;
// Simulate a user adding instructions while the response runs.
ws.send({
type: "response.steer",
previous_response_id: initialResponseId,
input:
"Keep the scope small enough for one developer to finish in two weeks.",
});
} else {
successorResponseId = event.response.id;
}
} else if (
["response.steer.failed", "response.failed", "error"].includes(
event.type
)
) {
reject(new Error(JSON.stringify(event)));
} else if (
event.type === "response.incomplete" &&
(event.response.id !== initialResponseId ||
event.response.incomplete_details?.reason !== "steered")
) {
reject(new Error(JSON.stringify(event)));
} else if (
event.type === "response.completed" &&
event.response.id === successorResponseId
) {
let text = "";
for (const item of event.response.output) {
if (item.type !== "message") continue;
for (const part of item.content) {
if (part.type === "output_text") text += part.text;
}
}
resolve(text);
}
// Acceptance only queues the input. Keep reading past the first response.
} catch (error) {
reject(error);
}
});
ws.send({
type: "response.create",
store: false,
model: "gpt-6-astra",
reasoning: { effort: "medium" },
input: "Draft a project plan for building a task-tracking app.",
});
});
console.log(output);
} finally {
clearTimeout(timeout);
ws.close();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53import asyncio
from openai import AsyncOpenAI
async def main():
client = AsyncOpenAI()
initial_response_id = None
successor_response_id = None
async with client.responses.connect() as connection:
await connection.response.create(
model="gpt-6-astra",
store=False,
reasoning={"effort": "medium"},
input="Draft a project plan for building a task-tracking app.",
)
async for event in connection:
if event.type == "response.created":
if initial_response_id is None:
initial_response_id = event.response.id
# Simulate a user adding instructions while the response runs.
await connection.response.steer(
previous_response_id=initial_response_id,
input="Keep the scope small enough for one developer to finish in two weeks.",
)
else:
successor_response_id = event.response.id
elif event.type in {"response.steer.failed", "response.failed", "error"}:
raise RuntimeError(event.to_json())
elif event.type == "response.incomplete":
response = event.response
if (
response.id != initial_response_id
or response.incomplete_details is None
or response.incomplete_details.reason != "steered"
):
raise RuntimeError(event.to_json())
elif (
event.type == "response.completed"
and event.response.id == successor_response_id
):
print(event.response.output_text)
return
# Acceptance only queues the input. Keep reading past the first response.
raise RuntimeError("Connection closed before the steered response finished.")
async def run():
await asyncio.wait_for(main(), timeout=120)
asyncio.run(run())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
client := openai.NewClient()
conn, err := client.Responses.Connect(ctx, responses.ResponseConnectionOptions{})
if err != nil {
log.Fatal(err)
}
defer conn.Close()
if err := conn.Create(ctx, responses.ResponsesClientEventResponseCreateParam{
Model: "gpt-6-astra",
Reasoning: shared.ReasoningParam{
Effort: shared.ReasoningEffortMedium,
},
Store: openai.Bool(false),
Input: responses.ResponsesClientEventResponseCreateInputUnionParam{
OfString: openai.String("Draft a project plan for building a task-tracking app."),
},
}); err != nil {
log.Fatal(err)
}
initialID, successorID := "", ""
for {
event, err := conn.Recv(ctx)
if err != nil {
log.Fatal(err)
}
switch event.Type {
case "response.created":
id := event.OfResponsesServerEventResponseWsCreated.Response.ID
if initialID == "" {
initialID = id
err := conn.Send(ctx, responses.ResponsesClientEventUnionParam{
OfResponseSteer: &responses.ResponseSteerEventParam{
PreviousResponseID: id,
Input: responses.ResponseSteerInputUnionParam{
OfString: openai.String("Keep the scope small enough for one developer to finish in two weeks."),
},
},
})
if err != nil {
log.Fatal(err)
}
} else {
successorID = id
}
case "response.steer.failed", "response.failed", "error":
log.Fatal(event.RawJSON())
case "response.incomplete":
r := event.OfResponsesServerEventResponseWsIncomplete.Response
if r.ID != initialID || r.IncompleteDetails.Reason != "steered" {
log.Fatal(event.RawJSON())
}
case "response.completed":
r := event.OfResponsesServerEventResponseWsCompleted.Response
if successorID != "" && r.ID == successorID {
fmt.Println(r.OutputText())
return
}
}
// Steering acceptance only queues input. Wait for the successor to complete.
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.responses.*;
import java.util.*;
var client = OpenAIOkHttpClient.fromEnv();
long deadline = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(120);
try (var conn =
client.async().responses().connect().get(10, java.util.concurrent.TimeUnit.SECONDS)) {
conn.send(
ResponsesClientEvent.ofResponseCreate(
ResponsesClientEvent.ResponseCreate.builder()
.model("gpt-6-astra")
.reasoning(
com.openai.models.Reasoning.builder()
.effort(com.openai.models.ReasoningEffort.MEDIUM)
.build())
.store(false)
.input("Draft a project plan for building a task-tracking app.")
.build()));
String initialId = null, successorId = null;
while (true) {
var event =
conn.receive()
.get(
Math.max(1, deadline - System.nanoTime()),
java.util.concurrent.TimeUnit.NANOSECONDS);
if (event.responseCreated().isPresent()) {
var id = event.responseCreated().orElseThrow().response().id();
if (initialId == null) {
initialId = id;
conn.send(
ResponsesClientEvent.ofResponseSteer(
ResponseSteerEvent.builder()
.previousResponseId(id)
.input(
"Keep the scope small enough for one developer to finish in two weeks.")
.build()));
} else {
successorId = id;
}
} else if (event.responseSteerFailed().isPresent()
|| event.responseFailed().isPresent()
|| event.error().isPresent()) {
throw new IllegalStateException(event.toString());
} else if (event.responseIncomplete().isPresent()) {
var response = event.responseIncomplete().orElseThrow().response();
if (!response.id().equals(initialId)
|| !response
.incompleteDetails()
.flatMap(Response.IncompleteDetails::reason)
.map(r -> r.toString().equals("steered"))
.orElse(false)) throw new IllegalStateException(event.toString());
} else if (event.responseCompleted().isPresent()
&& event.responseCompleted().orElseThrow().response().id().equals(successorId)) {
event.responseCompleted().orElseThrow().response().output().stream()
.flatMap(item -> item.message().stream())
.flatMap(message -> message.content().stream())
.flatMap(content -> content.outputText().stream())
.forEach(text -> System.out.println(text.text()));
break;
}
// Acceptance only queues input; follow the successor through completion.
}
} finally {
client.close();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45require "async"
require "openai"
client = OpenAI::Client.new
Sync do |task|
task.with_timeout(120) do
client.responses.connect(request_options: { timeout: 10 }) do |connection|
connection.response.create(
model: "gpt-6-astra", store: false, reasoning: { effort: "medium" },
input: "Draft a project plan for building a task-tracking app."
)
state = {}
while (event = connection.receive)
case event
when OpenAI::Responses::ResponseCreatedEvent
response = event.response
if !state[:initial_id]
state[:initial_id] = response.id
connection.send_event(
type: "response.steer", previous_response_id: state[:initial_id],
input: "Keep the scope small enough for one developer to finish in two weeks."
)
else
state[:successor_id] = response.id
end
when OpenAI::Responses::ResponseSteerFailedEvent, OpenAI::Responses::ResponseFailedEvent, OpenAI::Responses::ResponsesServerEvent::ResponseWsError
raise "Steering failed: #{event.to_json}"
when OpenAI::Responses::ResponseIncompleteEvent
response = event.response
unless response.id == state[:initial_id] && response.incomplete_details&.reason.to_s == "steered"
raise "Response incomplete: #{event.to_json}"
end
when OpenAI::Responses::ResponseCompletedEvent
response = event.response
next unless state[:successor_id] && response.id == state[:successor_id]
puts(response.output_text)
state[:completed] = true
break
end
end
raise "Connection closed before the steered response finished" unless state[:completed]
end
end
end
このサンプルでは、最初の response.created イベントを受信してから追加指示を送信します。実際のアプリケーションでは、ユーザーが追加指示を入力した時点で送信してください。後続のレスポンスの response.created イベントを受信したら、それ以降の追加指示にはそのレスポンスの ID を使用します。
レスポンスの処理にクライアント側のツールの実行結果または承認が必要な場合、API は追加指示をキューに保持します。同じ接続で、通常どおりツールの実行または承認の手順を進めてください。
たとえば、元のレスポンスは get_project_status の呼び出しを出力して完了することがあります。以下のペイロードには、関連するフィールドのみを示します。
123456789101112131415{
"type": "response.completed",
"response": {
"id": "resp_1",
"status": "completed",
"output": [
{
"type": "function_call",
"call_id": "call_project",
"name": "get_project_status",
"arguments": "{\"project\":\"task-tracker\"}"
}
]
}
}
元のレスポンスが完了した時点で、受け付け済みの追加指示を反映するためにまだ入力が必要な場合、API は response.steer.pending を送信します。required_input フィールドには、指示の反映に必要なツールの実行結果または承認が示されます。
12345678910111213141516{
"type": "response.steer.pending",
"sequence_number": 12,
"steer": {
"id": "steer_0123456789abcdef0123456789abcdef",
"previous_response_id": "resp_1"
},
"reason": "waiting_for_required_input",
"required_input": [
{
"type": "function_call_output",
"call_id": "call_project",
"name": "get_project_status"
}
]
}
同じ接続で response.create を送信し、必要な入力を返します。previous_response_id には resp_1 を設定してください。受け付け済みの追加指示は再送しないでください。明示的に送信した response.create には、そのリクエスト自体のツール、指示、その他の設定が適用されます。
次の JSONC サンプルでは、サーバーがキュー内の追加指示を挿入する位置をコメントで示しています。
123456789101112131415161718{
"type": "response.create",
"model": "gpt-6-astra",
"previous_response_id": "resp_1",
"input": [
// The server implicitly prepends your accepted steer here:
// "Keep the scope small enough for one developer to finish in two weeks."
{
"type": "function_call_output",
"call_id": "call_project",
"output": "Design is complete. Development has not started.",
},
{
"role": "user",
"content": "Show me the updated plan before starting any work.",
},
],
}
ツールの実行結果は、response.steer.pending を待たずに返せます。サーバーが対応する response.create をすでに受信していれば、この通知を先に送信せずに処理を続行できます。
response.steer.failed は、API が追加指示を反映できず、以降も自動的には反映しないことを示します。このイベントでは、元の input と previous_response_id が steer の下に返され、error オブジェクトに失敗の詳細が示されます。
受け付け済みの追加指示は steer.id で追跡してください。その後に失敗した場合も、失敗イベントには同じ ID が使用されます。
主なエラーコードは以下のとおりです。
invalid_input:サポート対象のイベントフィールドのみを使用し、入力にはユーザーメッセージを指定してください。
steering_not_supported:モデル、リクエストパラメーター、またはその両方が追加指示に対応していない可能性があります。
response_not_found:対象のレスポンスは、同じ WebSocket 接続上で引き続き参照できる必要があります。
too_many_pending_steers:保留中の追加指示が多すぎます。ツールの実行結果または承認が必要な場合は、response.create で返してください。それ以外の場合は、後続のレスポンスが自動的に作成されてから次の追加指示を送信してください。受け付け済みの追加指示は再送しないでください。
キュー内の追加指示は現在の接続でのみ保持され、元のレスポンスと一緒には保存されません。送信した追加指示を記録し、再送する前にレスポンスのイベントや履歴と照合してください。切断後も保留中の追加指示が残っているとは限りません。WebSocket の接続復旧手順を参照してください。