중간 턴 조정

중간 턴 조정 (Mid-turn steering)

출처: 문서

Mid-turn steering(중간 턴 조정)을 사용하면 사용자가 응답이 끝날 때까지 기다리지 않고 요구사항을 추가하거나 방향을 바꿀 수 있어요.

Mid-turn steering은 GPT-6 모델 제품군에서 Responses API로의 WebSocket 연결을 통해 사용할 수 있어요. GPT-5.6 및 이전 모델은 steering을 지원하지 않아요.

Steering은 이미 애플리케이션으로 보낸 출력을 다시 쓰거나, 이전 작업을 되돌리거나, 이미 시작된 도구를 취소하지 않아요.

연결 설정과 일반적인 전송 동작은 WebSocket mode를 참고하고, 정확한 이벤트 정의는 Responses WebSocket events reference를 참고해요.

Steering 메시지 보내기

response.create로 응답을 시작해요. response.created 이벤트를 받은 후, 같은 연결에서 해당 응답의 ID를 previous_response_id로 사용해 response.steer를 보내요:

{
  "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로 확인해 줘요:

{
  "type": "response.steer.accepted",
  "sequence_number": 4,
  "steer": {
    "id": "steer_0123456789abcdef0123456789abcdef",
    "previous_response_id": "resp_1"
  }
}

확인(acceptance)은 입력이 큐에 들어갔다는 뜻이지, 모델이 그것에 대해 작업했다는 뜻은 아니에요. API는 애플리케이션의 도구 결과나 승인이 필요하지 않는 한, 사용자의 업데이트로 자동으로 새 응답을 만들어요.

이 자동 연속(continuation)을 만들기 전에, 서버는 현재 출력 항목과 이미 실행 중인 호스팅 도구 작업을 먼저 마쳐요. 업데이트가 포함된 응답을 받으려면 이벤트를 계속 읽어요. 다른 response.create를 보내지 마세요.

Steering이 원래 응답을 방해하면, 응답은 response.incomplete와 incomplete_details.reason: "steered"로 끝나요. 원래 응답이 먼저 정상적으로 끝나면 완료 상태를 유지하며, 여전히 steering 연속을 가질 수 있어요.

자동 연속은 원래 요청 설정을 상속해요. 토큰 및 도구 호출 한도는 각 응답에 별도로 적용돼요.

완전한 예시 실행하기

.NET SDK는 Responses WebSocket 클라이언트를 제공하지 않으므로, 이 예시에는 C# SDK 변형이 없어요.

실행 중에 프로젝트 계획 업데이트하기

// 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",
      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();
}
import 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, asyncio.timeout(120):
        await connection.response.create(
            model="gpt-6-astra",
            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.")


asyncio.run(main())
require "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", 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 이벤트 후에 업데이트를 보내요. 애플리케이션에서는 사용자가 업데이트를 제공할 때 보내요. 새 steering에는 연속의 response.created 이벤트가 도착한 후 그 연속의 ID를 사용해요.

도구 결과 또는 승인 반환하기

응답에 클라이언트 도구 결과나 승인이 필요하면, API는 steering을 큐에 유지해요. 같은 연결에서 일반적인 도구 또는 승인 흐름을 계속 진행해요.

예를 들어 원래 응답은 get_project_status 호출로 완료될 수 있어요. 다음 페이로드는 관련 필드만 보여줘요:

{
  "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는 여전히 입력이 필요한 승인된 steering에 대해 response.steer.pending을 보내요. 그 required_input 필드는 API가 업데이트를 적용하기 전에 필요한 도구 결과나 승인을 식별해 줘요:

{
  "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로 설정해요. 승인된 steering을 반복하지 마세요. 명시적 response.create는 자체 도구, 지침 및 기타 설정을 사용해요.

이 JSONC 예시의 주석은 서버가 큐에 들어간 업데이트를 추가하는 위치를 보여줘요:

{
  "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가 steering을 통해 입력을 적용하지 않았고 나중에 자동으로 적용하지도 않는다는 뜻이에요. 이 이벤트는 steer 아래에 원래 input과 previous_response_id를 반환하며, 실패를 설명하는 error 객체가 포함돼요.

승인된 제출은 steer.id로 추적해요. 이후의 실패는 같은 ID를 사용해요.

일반적인 오류 코드:

  • invalid_input: 지원되는 이벤트 필드와 사용자 메시지 입력만 사용해요.
  • steering_not_supported: 모델, 요청 파라미터 또는 둘 다 steering과 호환되지 않을 수 있어요.
  • response_not_found: 대상 응답이 동일한 WebSocket 연결에서 계속 사용 가능해야 해요.
  • too_many_pending_steers: 너무 많은 steering 입력이 대기 중이에요. response.create로 필요한 도구 결과나 승인을 반환하거나, 그렇지 않으면 자동 연속을 기다린 후 더 제출해요. 이미 승인된 steering을 다시 보내지 마세요.

큐에 있는 steering 입력은 현재 연결에만 존재하며, 원래 응답과 함께 저장되지 않아요. 보낸 steering 입력을 기록하고, 재전송 전에 응답 이벤트 및 기록과 비교해요. 대기 중인 steering이 연결 끊김을 견뎌냈다고 가정하지 마세요. WebSocket 복구 지침을 참고해요.