Warning
Check the current sunset date for the Assistants API on OpenAI and on Azure OpenAI (they publish separately). After it, Assistants calls fail. Treat this as a planned migration, not a someday task.
Concept mapping
| Assistants API | Responses API |
|---|---|
| Assistant (model, instructions, tools) | instructions + tools in each request, or a stored prompt object |
| Thread | Conversation (or chain with previous_response_id) |
| Run (create, poll status) | One response call (sync or streamed); no polling loop |
| Run steps | Output items (messages, tool calls, tool results) |
| Messages on a thread | Items in a conversation |
file_search with vector stores | Same vector stores, file_search tool on the response |
code_interpreter | code_interpreter tool with a container |
Function calling (requires_action) | Function call items returned; send function_call_output items back |
1. Inventory
For each app: assistants (export their instructions, model, tools, vector store ids), how threads are stored and mapped to users, whether runs are polled or streamed, and which function tools exist.
for a in client.beta.assistants.list(limit=100):
print(a.id, a.name, a.model, [t.type for t in a.tools])
2. Rewrite the call path
Before (simplified):
thread = client.beta.threads.create()
client.beta.threads.messages.create(thread.id, role="user", content=q)
run = client.beta.threads.runs.create_and_poll(thread_id=thread.id, assistant_id=ASSISTANT_ID)
msgs = client.beta.threads.messages.list(thread.id, order="desc", limit=1)
After:
conv = client.conversations.create() # once per user session; store conv.id
resp = client.responses.create(
model="gpt-4.1", # Azure: your deployment name
instructions=ASSISTANT_INSTRUCTIONS,
conversation=conv.id,
input=q,
tools=[{"type": "file_search", "vector_store_ids": [VECTOR_STORE_ID]}],
)
print(resp.output_text)
Alternative without a conversation object: pass previous_response_id=resp.id on the next turn (requires stored responses).
3. Function tools
resp = client.responses.create(model=M, input=q, tools=[{"type": "function", "name": "get_ticket",
"description": "Get a ticket by id", "parameters": {"type": "object",
"properties": {"id": {"type": "string"}}, "required": ["id"], "additionalProperties": False}}])
calls = [i for i in resp.output if i.type == "function_call"]
outputs = [{"type": "function_call_output", "call_id": c.call_id,
"output": json.dumps(get_ticket(**json.loads(c.arguments)))} for c in calls]
resp = client.responses.create(model=M, previous_response_id=resp.id, input=outputs, tools=TOOLS)
No more requires_action / submit_tool_outputs polling.
4. Existing threads
Decide per app:
- Start fresh: acceptable for most support chats; keep old threads read-only until sunset for reference.
- Carry history: read thread messages and add them as items to a new conversation (text only; check how attachments should be handled).
Map your user ↔ thread table to user ↔ conversation.
5. Test
- Evaluation set through old and new paths; compare answers and file_search citations.
- Tool calls: arguments, multiple parallel calls, error outputs.
- Streaming events changed names and shape. Update the UI's event handling.
- Data retention settings for stored responses/conversations agree with your policy (
storeparameter).
6. Clean up
Remove polling loops, run-status handling and assistant-creation scripts; move instructions into source control (or managed prompts with versions).
Tip
Putting instructions in code (or a versioned prompt) instead of a mutable assistant object is a quiet improvement: prompt changes now go through review and your evaluation gate.
Checklist
- Sunset date known for your platform
- Assistants, tools, vector stores inventoried
- Call path rewritten (conversations or previous_response_id)
- Function tool loop and streaming UI updated
- Thread migration decision made
- Evaluation compared, old code removed