To migrate from Chat Completions to the Responses API, send requests to POST /v1/responses, move the system message into the top-level instructions parameter, pass your messages as input, and read response.output_text instead of choices[0].message.content. The rest of the migration is handling typed output items, deciding how to carry multi-turn state, reshaping function definitions and structured output settings, and updating streaming handlers. OpenAI's guide says Chat Completions remains supported and recommends migrating one user flow at a time.
Who this is for and what you will have at the end
This guide is for developers with working code on the Chat Completions API who want to move to the Responses API, whether for built-in tools, better reasoning-model support or because a newer model requires it. Examples use the Python SDK.
By the end you will have a checklist of every code change that matters, snippets for each, and a list of the migration bugs OpenAI's guide calls out. Model names below are placeholders, so substitute the model you use.
If you are moving from Azure rather than OpenAI directly, see Azure OpenAI Assistants API migration for a related migration, and note that Azure's feature support can differ from OpenAI's.
Prerequisites
- The official
openaiPython package, recent enough to includeclient.responses. - An existing Chat Completions call you can test against.
- A list of your tools, structured output schemas and any streaming code, since each needs a change.
Migrate one flow at a time. Because both APIs stay available, you can run old and new code side by side and compare results.
At a glance: what changes
| Area | Chat Completions | Responses API |
|---|---|---|
| Endpoint | /v1/chat/completions | /v1/responses |
| System prompt | system message in messages | Top-level instructions |
| Input | messages list | input: string or list of input items |
| Output | choices[].message | output array of typed items; output_text helper |
| Multiple candidates | n parameter | Not available; make separate requests |
| Multi-turn | Resend the transcript | Resend, or use previous_response_id or Conversations |
| Function definition | Nested under function | name, description, parameters at top level |
| Structured output | response_format | text.format |
| Streaming | delta chunks | Typed server-sent events |
| Hosted tools | Not available | Enabled via tools (for example web_search) |
Step 1: Change the endpoint and request shape
This is the smallest possible migration. Before:
completion = client.chat.completions.create(
model="your-model",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello!"},
],
)
text = completion.choices[0].message.contentAfter:
response = client.responses.create(
model="your-model",
instructions="You are a helpful assistant.",
input="Hello!",
)
text = response.output_textPoints to note:
inputaccepts a plain string or a list of input items. The guide says text-only message lists can often be passed asinputunchanged, so you do not always have to rewrite your history format.- The
nparameter is not available. If you need several candidate answers, make separate requests.
Step 2: Handle typed output items
Chat Completions returns choices[].message. Responses returns an output array of typed items. Item types include message, reasoning, function_call and function_call_output. Do not assume every item is a message.
- If you only need the final text, use
output_text. - Otherwise, iterate over
outputand branch on each item'stype:
for item in response.output:
if item.type == "message":
... # final assistant message
elif item.type == "function_call":
... # a tool the model wants you to run
elif item.type == "reasoning":
... # keep this if you replay context yourselfThis matters most for reasoning models and agents, where the output typically has more than one item.
Step 3: Decide how to carry multi-turn state
Chat Completions needs you to resend the whole transcript each turn. Responses gives you three options.
Option A: previous_response_id
OpenAI manages the earlier context and you reference the previous response:
res1 = client.responses.create(
model="your-model",
input="What is the capital of France?",
store=True,
)
res2 = client.responses.create(
model="your-model",
instructions="You are a helpful assistant.",
input="And its population?",
previous_response_id=res1.id,
store=True,
)Two caveats from the guide. First, previous_response_id does not carry over the previous response's top-level instructions, so resend them every time. Second, earlier input tokens in the chain are still billed as input tokens, so this does not make long conversations cheaper.
Option B: replay context manually
This is closest to what you did before, and gives you control to trim or edit the history:
context = [{"role": "user", "content": "What is the capital of France?"}]
res1 = client.responses.create(model="your-model", input=context)
context += res1.output # keep reasoning, function_call and message items
context.append({"role": "user", "content": "And its population?"})
res2 = client.responses.create(model="your-model", input=context)Append the whole output list, not only the message text. Dropping reasoning, function call or function call output items is one of the errors the guide lists.
Option C: Conversations API
For long-lived state, the guide points to a persistent conversation object through the Conversations API. See the OpenAI documentation for its current interface before you adopt it.
Storage and Zero Data Retention
The guide states that Responses are stored by default, and that Chat Completions is stored by default for new accounts. To turn storage off, set store=False. For stateless or Zero Data Retention (ZDR) setups, set store=False and preserve and replay every reasoning item you receive, each of which includes encrypted_content by default. For ZDR organizations, OpenAI enforces store: false automatically. Check your data handling requirements before choosing, since a stored response is not the same as a stateless one.
Step 4: Reshape function calling
Three changes affect tool code.
Definition shape. Chat Completions tags functions externally, under a function key. Responses tags them internally, with name, description and parameters at the top level:
tool = {
"type": "function",
"name": "get_weather",
"description": "Determine weather in my location",
"parameters": {
"type": "object",
"properties": {"location": {"type": "string"}},
"required": ["location"],
"additionalProperties": False,
},
}Strictness. Functions in Chat Completions are non-strict by default. In Responses, if you omit strict, the API attempts strict mode and falls back to non-strict when the schema is not compatible. If your code relies on non-strict behavior, set strict: false explicitly.
Results. Tool calls and their outputs are separate item types linked by call_id. Always send the matching call_id with each function result. A typical loop runs your function for each function_call item, appends the returned items to the context along with a function_call_output item carrying the same call_id, and calls the API again.
Step 5: Move structured outputs to text.format
Move response_format to text.format:
response = client.responses.create(
model="your-model",
input="Jane, 54 years old",
text={
"format": {
"type": "json_schema",
"name": "person",
"strict": True,
"schema": {
"type": "object",
"properties": {
"name": {"type": "string", "minLength": 1},
"age": {"type": "number", "minimum": 0, "maximum": 130},
},
"required": ["name", "age"],
"additionalProperties": False,
},
}
},
)The name, strict and schema fields sit directly inside format rather than under a nested json_schema object.
Step 6: Update streaming handlers
Chat Completions streams delta chunks. Responses streams typed server-sent events, so your handler should branch on each event's type. Events named in the guide include response.created, response.output_text.delta, response.completed, error, and the function-call argument events response.function_call_arguments.delta and its .done counterpart. Reusing a Chat Completions chunk handler is a listed migration error.
Step 7: Take advantage of native tools and reasoning support
Chat Completions cannot use OpenAI-hosted tools, so you had to build integrations yourself. With Responses you enable them in tools:
answer = client.responses.create(
model="your-model",
input="Who is the current president of France?",
tools=[{"type": "web_search"}],
)
print(answer.output_text)The guide also mentions file search, computer use, code interpreter, image generation and remote MCP servers as hosted tools. Availability and pricing vary by tool and model, so check the tool's own documentation before you depend on one.
For reasoning models, the guide says Responses gives better tool usage and recommends keeping reasoning items in context. It also lists model-specific constraints: starting with GPT-5.4, Chat Completions does not support tool calling with reasoning_effort values other than none, and the newest models named in the guide require the Responses API for tool calling. If you plan to upgrade models, this may decide the timing of your migration.
Verification
After migrating a flow, check that:
- A plain text request returns text through
output_text. - A multi-turn exchange gives a sensible second answer using your chosen state method.
- A tool call round-trips: the model emits a
function_call, you return afunction_call_outputwith the samecall_id, and you receive a final message. - A structured output request returns JSON that validates against your schema.
- Streaming shows text incrementally and finishes on
response.completed.
Compare answers and latency against the old path on a sample of real prompts before you switch traffic.
Troubleshooting: the common migration errors
OpenAI's guide lists these mistakes, and they explain most failures:
| Symptom | Cause | Fix |
|---|---|---|
AttributeError or empty text | Reading choices[0].message.content | Use output_text, or iterate output |
| Crash on unexpected item | Treating every output item as a message | Branch on item.type |
| Model forgets its tool calls or reasoning | Dropping reasoning, function call or output items when replaying | Append the whole output list to the context |
| Tool results ignored or rejected | Missing or wrong call_id | Return each result with its matching call_id |
| Schema not applied | Still passing response_format | Use text.format |
| Streaming output garbled or empty | Chat Completions chunk handler on Responses events | Branch on event type |
| Persona lost on turn two | instructions not resent with previous_response_id | Pass instructions every request |
| Bill higher than expected | Assuming previous_response_id avoids billing prior context | Budget for prior input tokens |
Migration checklist
- Endpoint changed to
client.responses.create - System message moved to
instructions;messagesmoved toinput - Any use of
nreplaced by separate requests - Output reading changed to
output_textor typed items - State approach chosen and
storeset intentionally - Tool definitions flattened;
strictset explicitly if you need non-strict -
call_idpreserved on every function result -
response_formatreplaced bytext.format - Streaming handler rewritten for typed events
- Tests run against real prompts before switching traffic
Comments
Questions and tips are welcome. Comments are checked by the EpiFive team before they appear; please do not share passwords, phone numbers or other private details.