사용자는 응답이 끝날 때까지 기다리지 않고 추가 지시를 보내 요구 사항을 더하거나 작업 방향을 바꿀 수 있습니다.
WebSocket으로 Responses API에 연결해 GPT-6 계열 모델을 사용하면 응답 생성 중에 지시를 변경할 수 있습니다. GPT-5.6 및 이전 모델은 이 기능을 지원하지 않습니다.
추가 지시를 보내도 애플리케이션에 이미 전송된 출력은 바뀌지 않습니다. 이전 작업이 되돌려지거나 이미 실행 중인 도구가 취소되지도 않습니다.
Java 예제를 실행하려면 Java 17 이상이 필요합니다. 연결 설정과 데이터 전송 방식은 WebSocket 모드를 참고하세요. 각 이벤트의 정확한 정의는 Responses WebSocket 이벤트 레퍼런스를 참고하세요.
response.create로 응답을 시작하세요. 해당 응답의 response.created 이벤트를 받으면 같은 연결에서 response.steer를 보내세요. 이때 해당 응답의 ID를 previous_response_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에는 문자열을 지정하거나, 지원되는 콘텐츠 유형의 사용자 메시지가 하나 이상 들어 있는 배열을 지정하세요.
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 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 필드에는 API가 지시를 반영하기 전에 받아야 하는 도구 결과나 승인이 표시됩니다.
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 연결 복구 가이드를 참고하세요.