Treat August 26, 2026 as a production deadline. OpenAI's deprecation notice says the Assistants API will be removed on that date and names the Responses API plus Conversations API as its replacement.
This guide covers only the official OpenAI Assistants API β Responses/Conversations production migration. Chat Completions β Responses, Azure OpenAI, and third-party OpenAI-compatible provider migrations have different contracts and are out of scope; do not reuse the object, tool, or retention conclusions here without separate verification.
The durable target is:
- application-owned, version-controlled instructions and tool definitions;
- Responses for model execution and typed output Items;
- Conversations when state must survive across sessions, devices, or jobs;
- an application tool loop for custom functions;
- measured File Search, streaming, retention, and failure parity;
- a feature-flagged cutover that can return to the legacy path.
Do not make reusable Prompt objects the long-lived destination. The current Assistants migration guide still presents an Assistant-to-Prompt step, but OpenAI announced reusable Prompt objects as deprecated on June 3, 2026, and schedules reusable Prompt objects plus v1/prompts to shut down on November 30, 2026. The durable route is to move prompt content into application code. Migrating Assistant β Prompt β code would create a second migration only three months later.
Define the destination before touching production
The old nouns do not map by renaming a URL:
| Assistants-era concern | Responses-era owner | Evidence required before cutover |
|---|---|---|
| Assistant instructions, model, and tools | Versioned application configuration | Golden inputs still satisfy behavior and refusal rules |
| Thread | Conversation, response chain, or application-managed Items | State survives exactly where the product requires it |
| Message | Typed input/output Items | Messages, reasoning, calls, and outputs are not flattened into text |
| Run | Response execution | Completion, failure, timeout, and cancellation have defined product behavior |
| Run step | An Item in response.output | Consumers branch on type |
| Tool output submission | A new function_call_output Item | Original call_id is preserved |
| Retrieval | Hosted file_search over verified vector stores | Correct files, chunks, citations, filters, latency, and empty-result behavior |
| Polling every Run | Synchronous call, streaming, or explicit background mode | Only genuinely long work uses background execution |
Inventory one production Assistant before editing its code. Capture its configuration, vector stores, custom tool schemas, active Thread ownership, cancellation behavior, and a golden set of successful and failing tasks. Record baseline answer quality, first-token and total latency, input/output tokens, tool success, and error rate. Without a baseline, a successful Response only proves that the new endpoint accepted a request.
If the project does not yet have a working Platform credential and API Billing, complete the OpenAI API key and first-request checklist first. Extra keys do not solve a state or tool-loop migration.
Pick one conversation contract
OpenAI's conversation-state guide documents three practical strategies:
Durable Conversation objects
Use a Conversation when the product must resume across requests, devices, or jobs. Conversations can retain messages, tool calls, tool outputs, and other Items. They are not subject to the ordinary Response object's default 30-day TTL, so a durable identifier also creates a durable retention obligation. Verify deletion, access control, residency, and the organization's current data contract before adopting it.
Short chains with previous_response_id
This is convenient for a bounded continuation. It is not free history compression: earlier input tokens in the chain are still billed as input. Top-level instructions do not carry forward automatically, so resend stable instructions. Put a maximum turn/token boundary on this route; move to a different state policy when the chain exceeds it.
Application-managed Items with store:false
Use this when ZDR, explicit pruning, or application custody is required. Preserve the complete response.output, including reasoning and tool Items, not just output_text. Replaying only the visible assistant text can silently remove state that the next reasoning or tool turn needs.
Long-running conversations also need a context policy. OpenAI's Compaction guide describes server-side thresholds and explicit /responses/compact. Test that a compacted session preserves durable facts, open tasks, tool state, and safety constraints before relying on it.
One matched scenario: Assistants before and Responses after
Both snippets use the same contract: input Where is ORDER-42?, strict tool schema get_order_status(order_id: string), and an expected result in which the tool runs once and returns shipped. Either path fails if it selects the wrong tool, accepts invalid arguments, loses the original call ID, exceeds six rounds, or invents a status without tool evidence.
The code is aligned with current OpenAI documentation and SDK examples, but this run did not execute it with the reader's credentials, project, Assistant ID, or current SDK. It is not evidence of production parity. Run both paths in staging with the current SDK and record the SDK version, response Items, call IDs, final output, and failure cases.
Before: Assistant, Thread, Run, and requires_action
OPENAI_ASSISTANT_ID must point to an existing legacy Assistant whose instructions and tool schema have been checked against the INSTRUCTIONS and TOOLS in the after example. Do not create a new Assistant merely to run this snapshot.
pythonimport json import os from openai import OpenAI client = OpenAI() assistant_id = os.environ["OPENAI_ASSISTANT_ID"] def get_order_status(order_id: str) -> dict: demo = {"ORDER-42": {"status": "shipped", "carrier": "Example Express"}} return demo.get(order_id, {"status": "not_found"}) thread = client.beta.threads.create() client.beta.threads.messages.create( thread_id=thread.id, role="user", content="Where is ORDER-42?", ) run = client.beta.threads.runs.create_and_poll( thread_id=thread.id, assistant_id=assistant_id, ) for _ in range(6): if run.status == "requires_action": calls = run.required_action.submit_tool_outputs.tool_calls outputs = [] for call in calls: if call.function.name != "get_order_status": raise RuntimeError(f"unexpected tool: {call.function.name}") args = json.loads(call.function.arguments) outputs.append({ "tool_call_id": call.id, "output": json.dumps(get_order_status(**args)), }) run = client.beta.threads.runs.submit_tool_outputs_and_poll( thread_id=thread.id, run_id=run.id, tool_outputs=outputs, ) else: break if run.status != "completed": raise RuntimeError(f"legacy run failed: {run.status}")
The Assistant owns configuration, the Thread owns state, and the Run advances execution. On requires_action, the application executes the function and submits output under the legacy tool_call_id.
After: application config, Conversation, Response, and tool loop
pythonimport json import os from openai import OpenAI client = OpenAI() MODEL = os.environ["OPENAI_MODEL"] INSTRUCTIONS = "Use get_order_status for order lookups. Do not invent status." TOOLS = [{ "type": "function", "name": "get_order_status", "description": "Return the current status for one order.", "strict": True, "parameters": { "type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"], "additionalProperties": False, }, }] def get_order_status(order_id: str) -> dict: demo = {"ORDER-42": {"status": "shipped", "carrier": "Example Express"}} return demo.get(order_id, {"status": "not_found"}) conversation = client.conversations.create( metadata={"app_session": "migration-smoke-001"} ) response = client.responses.create( model=MODEL, conversation=conversation.id, instructions=INSTRUCTIONS, tools=TOOLS, input="Where is ORDER-42?", ) for _ in range(6): calls = [item for item in response.output if item.type == "function_call"] if not calls: print(response.output_text) break outputs = [] for call in calls: if call.name != "get_order_status": raise RuntimeError(f"unexpected tool: {call.name}") args = json.loads(call.arguments) outputs.append({ "type": "function_call_output", "call_id": call.call_id, "output": json.dumps(get_order_status(**args)), }) response = client.responses.create( model=MODEL, conversation=conversation.id, instructions=INSTRUCTIONS, tools=TOOLS, input=outputs, ) else: raise RuntimeError("tool loop exceeded 6 rounds")
The after path moves instructions, model selection, and schema into versioned application configuration; uses a Conversation for this scenario's state; and represents execution as a Response plus typed Items. The application validates and executes every custom function, then returns function_call_output under the original call_id. If the application owns history instead of a Conversation, preserve the complete response.output, not just visible text.
Keep three tool execution contracts separate
| Tool type | Who executes it | Application responsibilities | What it does not prove |
|---|---|---|---|
| Custom function | Your application; Responses emits a function_call | Validate, authorize, time out, make side effects idempotent, audit, cap rounds, and return function_call_output with the original call_id | Receiving a call does not mean the business action ran |
| Hosted / built-in tool | OpenAI executes it inside the Response under that tool's current contract, such as file_search here | Verify model/project support, scope, output Items, citations, errors, cost, and data boundaries | A 200 does not prove retrieval or task parity |
| Remote MCP | The Responses API calls a configured, trusted remote MCP server and records an mcp_call Item | Establish server/OAuth trust, constrain allowed_tools, review approvals and shared data, log use, and handle third-party retention and failures | OpenAI does not verify a third-party server's behavior, data policy, or side effects for you |
The function-calling guide defines the custom-function loop. The MCP and Connectors guide defines remote-server, approval, and security boundaries. MCP approval is configurable; do not disable it for sensitive actions without a deliberate review.
Prove File Search independently
Hosted file_search is not the same as a custom function loop. According to the official File Search guide, Responses can search specified vector stores; include=["file_search_call.results"] exposes retrieval results, while the resulting message can contain file citations.
This Python smoke test keeps both model and vector-store identifiers outside source:
pythonimport os from openai import OpenAI client = OpenAI() MODEL = os.environ["OPENAI_MODEL"] VECTOR_STORE_ID = os.environ["OPENAI_VECTOR_STORE_ID"] response = client.responses.create( model=MODEL, input="Which section defines the refund approval limit?", tools=[{ "type": "file_search", "vector_store_ids": [VECTOR_STORE_ID], }], include=["file_search_call.results"], ) if response.status != "completed": raise RuntimeError(f"file search failed: {response.status}") print(response.output_text) for item in response.output: print(item.type, item)
Use a fixed retrieval test set and check:
- every referenced vector store belongs to the intended account/project;
- files are processed and available before traffic arrives;
- expected source files and passages are returned;
- message annotations cite the correct files;
- metadata filters and empty-result behavior match product policy;
- stale, conflicting, and unauthorized documents do not become answers;
- P50/P95 latency, token use, tool-call count, and error rate remain within guardrails.
Do not compensate for degraded retrieval by adding a larger prompt or more chunks. Stop the rollout and fix index ownership, filters, file readiness, or evaluation coverage.
Move new chats first; backfill history selectively
OpenAI does not provide an automatic Thread-to-Conversation migration tool. Its current guide recommends moving new chats to Responses and Conversations, then backfilling older Threads only as needed.
Use a business rule:
- Active and resumable: convert supported messages, retain a legacy Thread ID β new Conversation ID mapping, and sample the result.
- Audit-only: leave the transcript in the application's read-only archive; do not feed it back to the model.
- Expired or unverifiable: do not backfill; apply the existing retention policy.
The official example focuses on message content. Attachments, tool events, metadata, unsupported legacy content, and business-side state require separate handling. If semantic fidelity cannot be demonstrated, keep history readable instead of claiming it is safely resumable.
Port streaming and long work by behavior
Do not replace every Run poller with the same Responses pattern.
- A normal task can await one Response.
- A live UI should consume typed events such as
response.created,response.output_text.delta,response.completed, anderror. - Function streams also emit function-argument events; a text-only delta handler is incomplete.
- Structured Outputs move to
text.format; continuing to send a legacyresponse_formatis a migration error. - Genuinely long work can opt into
background:true, then retrieve or cancel it according to the Background mode guide.
For each behavior, include a failing case: a cancelled job, an invalid tool argument, a schema violation, an empty retrieval, and a stream interrupted before response.completed. Define the user-visible outcome and retry boundary instead of trusting the absence of an SDK exception.
Cut over only when the acceptance board is green
Run the legacy and new implementations on the same golden inputs in shadow mode. Return only the legacy answer to users while the new path records de-identified comparison metrics. Promote traffic only when every required row passes:
| Contract | Pass evidence | Rollback trigger |
|---|---|---|
| Core behavior | Required facts, refusals, and escalation rules remain correct | Material quality regression |
| Conversation isolation | Context persists within one session and never crosses sessions | Lost context or cross-user leakage |
| Custom functions | Name, arguments, call_id, output, retries, and max rounds are correct | Wrong execution or unbounded loop |
| File Search | Correct documents, passages, citations, filters, and empty result | Unsupported or unauthorized answer |
| Streaming/background | Start, deltas, completion, failure, cancellation, and UI recovery work | Hung or false-complete task |
| Structured output | Schema validates without silent fallback | Invalid payload reaches downstream code |
| Long context | Growth is monitored; compaction preserves durable state | Context or cost exceeds the cap |
| Data contract | Store, access, delete, TTL, and ZDR behavior are approved | Retention boundary is uncertain |
| Operations | Quality holds; P95 latency, cost, and errors stay within limits | Any guardrail breaches |
A practical progression is internal accounts β 1% low-risk traffic β 5% β 25% β 100%. Hold each stage for a complete business cycle. Keep the legacy feature flag and objects until the observation window closes; deleting them first destroys the rollback path.
The migration is complete when migrated traffic no longer creates Assistants, Threads, or Runs; state has a named owner; custom tools and File Search pass the golden set; streaming, failures, and cancellation are observable; cost and retention have explicit limits; and one switch can restore the old path during the agreed rollback window.



