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)을 물려받아요. 프롬프트가 도구 스키마와 구조화된 출력 기대치를 캡슐화하기 때문이에요.
실질적인 마이그레이션 단계
- 각 기존 Assistant의 instruction + tool 묶음을 식별해요.
- 대시보드에서 그 묶음을 이름 있는 프롬프트로 재생성해요.
- 프롬프트 ID(또는 내보낸 사양)를 소스 컨트롤에 저장해 애플리케이션 코드가 안정적인 식별자를 참조할 수 있게 해요.
- 배포 중에 프롬프트 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. 어시스턴트에서 프롬프트 만들기
- 애플리케이션에서 가장 중요한 assistant 객체를 식별해요.
- 대시보드에서 이 객체들을 찾아
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)를 참고하세요.