Assistants API 마이그레이션 가이드

Assistants API 마이그레이션 가이드

Assistants API가 2026년 8월 26일 공식 종료(sunset)되어 더 이상 사용할 수 없어요. 새 통합에는 Responses API를 사용하며, 기존 통합을 이전하는 방법을 다루는 가이드랍니다.

출처: 문서

본문

Assistants API는 2026년 8월 26일에 공식적으로 종료되었으며 더 이상 사용할 수 없어요. 새 통합에는 Responses API를 사용하세요.

Assistants API를 사용해 주신 모든 분들께 감사드립니다. 여러분이 만든 모든 것과 함께 나눠 주신 피드백에 감사드립니다.

이 가이드를 사용해 통합을 Responses API로 이전하세요.

Responses는 더 간단해요. 입력 아이템을 보내면 출력 아이템을 돌려받죠. Responses API를 사용하면 더 나은 성능과 deep research, MCP, computer use 같은 새로운 기능도 얻을 수 있어요. 이 변경으로 previous_response_id를 넘겨주는 대신 대화(conversation)를 관리할 수도 있게 됐어요.

무엇이 바뀌었나요?

Before Now Why?
Assistants Prompts Prompts가 설정(configuration: 모델, 도구, 지침)을 보관하며 버전 관리와 업데이트가 더 쉬워요
Threads Conversations 메시지만이 아니라 아이템의 스트림을 다뤄요
Runs Responses Responses는 입력 아이템을 보내거나 conversation 객체를 사용해 출력 아이템을 받으며, 도구 호출 루프를 명시적으로 관리해요
Run steps Items 일반화된 객체 — 메시지, 도구 호출, 출력 등이 될 수 있어요

어시스턴트에서 프롬프트로 (From assistants to prompts)

Assistants는 모델 선택, 지침, 도구 선언을 묶어 놓은 영구적인 API 객체였으며, 전적으로 API를 통해 생성되고 관리됐어요. 그 대체물인 prompts는 대시보드에서만 생성할 수 있으며, 제품을 개발하면서 버전 관리할 수 있어요.

이것이 유용한 이유

  • 이식성과 버전 관리(Portability and versioning): 프롬프트 사양을 스냅샷, 검토, diff, 롤백할 수 있어요. 프롬프트를 버전 관리할 수도 있으므로 코드는 최신 버전을 가리키면 돼요.
  • 관심사 분리(Separation of concerns): 애플리케이션 코드는 이제 오케스트레이션(히스토리 정리, 도구 루프, 재시도)을 처리하고, 프롬프트는 높은 수준의 동작과 제약(시스템 안내, 도구 가용성, 구조화된 출력 스키마, temperature 기본값)에 집중해요.
  • Realtime 호환성: Realtime API로 연결할 때도 동일한 프롬프트 설정을 재사용할 수 있어, 채팅, 스트리밍, 저지연 대화형 세션 전반에 걸쳐 동작에 대한 단일 정의를 제공해요.
  • 도구와 출력의 일관성: 프롬프트를 사용하면, 시작하는 모든 Responses 또는 Realtime 세션이 일관된 계약(contract)을 물려받아요. 프롬프트가 도구 스키마와 구조화된 출력 기대치를 캡슐화하기 때문이에요.

실질적인 마이그레이션 단계

  1. 각 기존 Assistant의 instruction + tool 묶음을 식별해요.
  2. 대시보드에서 그 묶음을 이름 있는 프롬프트로 재생성해요.
  3. 프롬프트 ID(또는 내보낸 사양)를 소스 컨트롤에 저장해 애플리케이션 코드가 안정적인 식별자를 참조할 수 있게 해요.
  4. 배포 중에 프롬프트 ID를 교체해 A/B 테스트를 실행해요. 프로그램적으로 assistant 객체를 만들거나 삭제할 필요가 없어요.

프롬프트를 Responses 또는 Realtime API에 끼워 넣을 수 있는 **버전 관리된 동작 프로필(versioned behavioral profile)**로 생각하세요.


스레드에서 대화로 (From threads to conversations)

스레드(thread)는 서버 측에 저장된 메시지 모음이었어요. 스레드는 메시지만 저장할 수 있었죠. 대화(conversation)는 메시지, 도구 호출, 도구 출력, 기타 데이터를 포함할 수 있는 아이템을 저장해요.

요청 예시

Python

Go

응답 예시

Thread 객체

{
  "id": "thread_CrXtCzcyEQbkAcXuNmVSKFs1",
  "object": "thread",
  "created_at": 1752855924,
  "metadata": {
    "user_id": "peter_le_fleur"
  },
  "tool_resources": {}
}

Conversation 객체

{
	"id": "conv_68542dc602388199a30af27d040cefd4087a04b576bfeb24",
	"object": "conversation",
	"created_at": 1752855924,
	"metadata": {
		"user_id": "peter_le_fleur"
	}
}

런에서 응답으로 (From runs to responses)

런(run)은 스레드에 대해 실행되는 비동기 프로세스였어요. 아래 예시를 참고하세요. Responses는 더 간단해요. 실행할 입력 아이템 세트를 제공하면 출력 아이템 목록을 돌려받죠.

Responses는 단독으로 사용하도록 설계되었지만, 맥락과 설정을 저장하기 위해 prompt 및 conversation 객체와 함께 사용할 수도 있어요.

요청 예시

Python

Go

응답 예시

Run 객체

{
  "id": "run_FKIpcs5ECSwuCmehBqsqkORj",
  "assistant_id": "asst_8fVY45hU3IM6creFkVi5MBKB",
  "cancelled_at": null,
  "completed_at": 1752857327,
  "created_at": 1752857322,
  "expires_at": null,
  "failed_at": null,
  "incomplete_details": null,
  "instructions": null,
  "last_error": null,
  "max_completion_tokens": null,
  "max_prompt_tokens": null,
  "metadata": {},
  "model": "gpt-4.1",
  "object": "thread.run",
  "parallel_tool_calls": true,
  "required_action": null,
  "response_format": "auto",
  "started_at": 1752857324,
  "status": "completed",
  "thread_id": "thread_CrXtCzcyEQbkAcXuNmVSKFs1",
  "tool_choice": "auto",
  "tools": [],
  "truncation_strategy": {
    "type": "auto",
    "last_messages": null
  },
  "usage": {
    "completion_tokens": 130,
    "prompt_tokens": 34,
    "total_tokens": 164,
    "prompt_token_details": {
      "cached_tokens": 0
    },
    "completion_tokens_details": {
      "reasoning_tokens": 0
    }
  },
  "temperature": 1.0,
  "top_p": 1.0,
  "tool_resources": {},
  "reasoning_effort": null
}

Response 객체

{
  "id": "resp_687a7b53036c819baad6012d58b39bcb074adcd9e24850fc",
  "created_at": 1752857427,
  "conversation": {
    "id": "conv_689667905b048191b4740501625afd940c7533ace33a2dab"
  },
  "error": null,
  "incomplete_details": null,
  "instructions": null,
  "metadata": {},
  "model": "gpt-5.5",
  "object": "response",
  "output": [
    {
      "id": "msg_687a7b542948819ba79e77e14791ef83074adcd9e24850fc",
      "content": [
        {
          "annotations": [],
          "text": "The \"5 Ds of Dodgeball\" are a humorous set of rules made famous by the 2004 comedy film **\"Dodgeball: A True Underdog Story.\"** In the movie, dodgeball coach Patches O'Houlihan teaches these basics to his team. The **5 Ds** are:\n\n1. **Dodge**\n2. **Duck**\n3. **Dip**\n4. **Dive**\n5. **Dodge** (yes, dodge is listed twice for emphasis!)\n\nIn summary:  \n> **\"If you can dodge a wrench, you can dodge a ball!\"**\n\nThese 5 Ds are not official competitive rules, but have become a fun and memorable pop culture reference for the sport of dodgeball.",
          "type": "output_text",
          "logprobs": []
        }
      ],
      "role": "assistant",
      "status": "completed",
      "type": "message"
    }
  ],
  "parallel_tool_calls": true,
  "temperature": 1.0,
  "tool_choice": "auto",
  "tools": [],
  "top_p": 1.0,
  "background": false,
  "max_output_tokens": null,
  "previous_response_id": null,
  "reasoning": {
    "effort": null,
    "generate_summary": null,
    "summary": null
  },
  "service_tier": "scale",
  "status": "completed",
  "text": {
    "format": {
      "type": "text"
    }
  },
  "truncation": "disabled",
  "usage": {
    "input_tokens": 17,
    "input_tokens_details": {
      "cached_tokens": 0
    },
    "output_tokens": 150,
    "output_tokens_details": {
      "reasoning_tokens": 0
    },
    "total_tokens": 167
  },
  "user": null,
  "max_tool_calls": null,
  "store": true,
  "top_logprobs": 0
}

통합 마이그레이션하기

기능 지원을 잃지 않고 Assistants API에서 Responses API로 이동하려면 아래 마이그레이션 단계를 따르세요.

1. 어시스턴트에서 프롬프트 만들기

  1. 애플리케이션에서 가장 중요한 assistant 객체를 식별해요.
  2. 대시보드에서 이 객체들을 찾아 Create prompt를 클릭해요.

이렇게 하면 각 기존 assistant 객체로부터 prompt 객체가 생성돼요.

재사용 가능한 prompt 객체(reusable prompt objects)도 폐기 예정이에요. 이 마이그레이션 경로를 사용한다면, 장기 운영 통합에서 prompt 객체를 채택하기 전에 prompts deprecation timeline을 검토하세요.

2. 새 사용자 채팅을 conversations와 responses로 옮기기

Conversations API와 Responses API로 새 채팅을 시작하세요. 이전 대화 기록을 보존하려면, 여러분의 애플리케이션이 이미 저장한 메시지를 사용해요.

아래 예시는 종료 전에 스레드 기록이 어떻게 마이그레이션될 수 있는지 보여줘요. 스레드 메시지를 가져오는 Assistants API 호출은 더 이상 동작하지 않으므로, 저장된 메시지를 대신 사용하세요.

# Replace the illustrative IDs and URLs below with your own resource values.

from openai import OpenAI

openai = OpenAI()
messages = []
thread_id = "thread_123"

for page in openai.beta.threads.messages.list(
    thread_id=thread_id, order="asc"
).iter_pages():
    messages += page.data

items = []
for m in messages:
    item = {"role": m.role}
    item_content = []

    for content in m.content:
        match content.type:
            case "text":
                item_content_type = "input_text" if m.role == "user" else "output_text"
                item_content += [
                    {"type": item_content_type, "text": content.text.value}
                ]
            case "image_url":
                item_content += [
                    {
                        "type": "input_image",
                        "image_url": content.image_url.url,
                        "detail": content.image_url.detail,
                    }
                ]

    item |= {"content": item_content}
    items.append(item)

# create a conversation with your converted items
conversation = openai.conversations.create(items=items)
# Replace the illustrative IDs and URLs below with your own resource values.
require "openai"

client = OpenAI::Client.new
thread_id = "thread_123"
messages = client.beta.threads.messages.list(thread_id, order: :asc)
items = []
messages.auto_paging_each do |message|
  content = message.content.filter_map do |part|
    case part
    when OpenAI::Models::Beta::Threads::TextContentBlock
      type = if message.role == OpenAI::Models::Beta::Threads::Message::Role::USER
               :input_text
             else
               :output_text
             end
      {
        type: type,
        text: part.text.value
      }
    when OpenAI::Models::Beta::Threads::ImageURLContentBlock
      {
        type: :input_image,
        image_url: part.image_url.url,
        detail: part.image_url.detail
      }
    end
  end
  items << {
    role: message.role,
    content: content
  }
end
conversation = client.conversations.create(
  items: items
)
puts(conversation.id)

전체 예시 비교하기

다음은 Assistants API와 Responses API를 모두 사용하는 통합 예시들로, 어떻게 비교되는지 볼 수 있어요.

사용자 채팅 앱

Assistants API

# Replace the illustrative IDs and URLs below with your own resource values.
threads_by_session: dict[str, str] = {}


@app.post("/messages")
async def message(message: Message):
    thread_id = threads_by_session.get(message.session_id)
    if thread_id is None:
        thread_id = openai.beta.threads.create().id
        threads_by_session[message.session_id] = thread_id

    openai.beta.threads.messages.create(
        thread_id=thread_id,
        role="user",
        content=message.content,
    )

    example_assistant_id = "asst_123"
    run = openai.beta.threads.runs.create(
        assistant_id=example_assistant_id,
        thread_id=thread_id,
    )
    while run.status in ("queued", "in_progress"):
        await asyncio.sleep(1)
        run = openai.beta.threads.runs.retrieve(
            thread_id=thread_id,
            run_id=run.id,
        )

    messages = openai.beta.threads.messages.list(
        order="desc",
        limit=1,
        thread_id=thread_id,
    )

    return {"content": messages.data[0].content}
# Replace the illustrative IDs and URLs below with your own resource values.
require "openai"

client = OpenAI::Client.new
assistant_id = "asst_123"
threads_by_session = {}

handle_message = lambda do |session_id:, content:|
  thread_id = threads_by_session[session_id]
  unless thread_id
    thread_id = client.beta.threads.create.id
    threads_by_session[session_id] = thread_id
  end

  client.beta.threads.messages.create(
    thread_id,
    role: :user,
    content: content
  )
  run = client.beta.threads.runs.create(
    thread_id,
    assistant_id: assistant_id
  )
  while [:queued, :in_progress].include?(run.status)
    sleep(1)
    run = client.beta.threads.runs.retrieve(run.id, thread_id: thread_id)
  end

  messages = client.beta.threads.messages.list(
    thread_id,
    order: :desc,
    limit: 1
  )
  { content: messages.data&.first&.content }
end

puts(
  handle_message.call(
    session_id: "example-session",
    content: "What are the five Ds of dodgeball?"
  )
)

Responses API

// Replace the illustrative IDs and URLs below with your own resource values.
import express from "express";
import OpenAI from "openai";

const app = express();
const client = new OpenAI();
const conversationsBySession = new Map();

app.use(express.json());

app.post("/messages", async (request, response) => {
  const { content, session_id: sessionId } = request.body ?? {};
  if (
    typeof content !== "string" ||
    !content.trim() ||
    typeof sessionId !== "string" ||
    !sessionId.trim()
  ) {
    response.status(400).json({
      error: "content and session_id must be non-empty strings.",
    });
    return;
  }

  let conversationIdPromise = conversationsBySession.get(sessionId);

  if (!conversationIdPromise) {
    conversationIdPromise = client.conversations
      .create()
      .then((conversation) => conversation.id)
      .catch((error) => {
        conversationsBySession.delete(sessionId);
        throw error;
      });
    conversationsBySession.set(sessionId, conversationIdPromise);
  }
  const conversationId = await conversationIdPromise;

  const promptId = "pmpt_123";

  const result = await client.responses.create({
    prompt: { id: promptId },
    input: [{ role: "user", content }],
    conversation: conversationId,
  });

  response.json({ content: result.output_text });
});

app.listen(Number(process.env.OPENAI_EXAMPLE_PORT ?? 8000), "127.0.0.1");
# Replace the illustrative IDs and URLs below with your own resource values.
conversations_by_session: dict[str, str] = {}


@app.post("/messages")
async def message(message: Message):
    conversation_id = conversations_by_session.get(message.session_id)
    if conversation_id is None:
        conversation_id = openai.conversations.create().id
        conversations_by_session[message.session_id] = conversation_id

    example_prompt_id = "pmpt_123"
    response = openai.responses.create(
        prompt={"id": example_prompt_id},
        input=[{"role": "user", "content": message.content}],
        conversation=conversation_id,
    )

    return {"content": response.output_text}
# Replace the illustrative IDs and URLs below with your own resource values.
require "openai"

client = OpenAI::Client.new
conversations_by_session = {}

handle_message = lambda do |session_id:, content:|
  conversation_id = conversations_by_session[session_id]
  unless conversation_id
    conversation_id = client.conversations.create.id
    conversations_by_session[session_id] = conversation_id
  end

  response = client.responses.create(
    prompt: { id: "pmpt_123" },
    input: [
      {
        role: :user,
        content: content
      }
    ],
    conversation: conversation_id
  )
  { content: response.output_text }
end

puts(
  handle_message.call(
    session_id: "example-session",
    content: "What are the five Ds of dodgeball?"
  )
)

더 알아보기 (Learn more)

관련 문서: Responses API로 마이그레이션과 기능 폐기(feature deprecations)를 참고하세요.