AI engineering

Migrate from OpenAI Chat Completions to the Responses API: What Changes

Port Chat Completions code to the OpenAI Responses API: new request shape, typed output items, multi-turn state, function calling, structured outputs and streaming.

By EpiFive Editorial TeamReviewed/updated 9 min readHow we check articles
On this page

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 openai Python package, recent enough to include client.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

AreaChat CompletionsResponses API
Endpoint/v1/chat/completions/v1/responses
System promptsystem message in messagesTop-level instructions
Inputmessages listinput: string or list of input items
Outputchoices[].messageoutput array of typed items; output_text helper
Multiple candidatesn parameterNot available; make separate requests
Multi-turnResend the transcriptResend, or use previous_response_id or Conversations
Function definitionNested under functionname, description, parameters at top level
Structured outputresponse_formattext.format
Streamingdelta chunksTyped server-sent events
Hosted toolsNot availableEnabled 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.content

After:

response = client.responses.create(
    model="your-model",
    instructions="You are a helpful assistant.",
    input="Hello!",
)
text = response.output_text

Points to note:

  • input accepts a plain string or a list of input items. The guide says text-only message lists can often be passed as input unchanged, so you do not always have to rewrite your history format.
  • The n parameter 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 output and branch on each item's type:
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 yourself

This 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 a function_call_output with the same call_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:

SymptomCauseFix
AttributeError or empty textReading choices[0].message.contentUse output_text, or iterate output
Crash on unexpected itemTreating every output item as a messageBranch on item.type
Model forgets its tool calls or reasoningDropping reasoning, function call or output items when replayingAppend the whole output list to the context
Tool results ignored or rejectedMissing or wrong call_idReturn each result with its matching call_id
Schema not appliedStill passing response_formatUse text.format
Streaming output garbled or emptyChat Completions chunk handler on Responses eventsBranch on event type
Persona lost on turn twoinstructions not resent with previous_response_idPass instructions every request
Bill higher than expectedAssuming previous_response_id avoids billing prior contextBudget for prior input tokens

Migration checklist

  • Endpoint changed to client.responses.create
  • System message moved to instructions; messages moved to input
  • Any use of n replaced by separate requests
  • Output reading changed to output_text or typed items
  • State approach chosen and store set intentionally
  • Tool definitions flattened; strict set explicitly if you need non-strict
  • call_id preserved on every function result
  • response_format replaced by text.format
  • Streaming handler rewritten for typed events
  • Tests run against real prompts before switching traffic

References

Questions people ask

Is the Chat Completions API being removed?

OpenAI's migration guide says Chat Completions remains supported and that the Responses API is recommended for new projects. It also says to migrate one user flow at a time, and that some newer models require Responses for tool calling.

How do I get the text of the reply from the Responses API?

Use response.output_text for the final text. If you need more, iterate over response.output and branch on each item's type, because the array can contain reasoning and function call items as well as messages.

Does previous_response_id make earlier turns free?

No. The guide notes that prior input tokens in the chain are still billed as input tokens. It also does not carry over the earlier response's top-level instructions, so you resend them on each request.

Where did response_format go?

In the Responses API, structured output settings move to text.format. For a JSON schema you pass type, name, strict and schema inside that object.

OpenAI APIResponses APIChat CompletionsPython
  1. LLM API Timeouts, Retries and Rate Limits: A Python Reliability Guide

    Make Claude and OpenAI API calls in Python resilient: set timeouts, retry only the right errors, honor retry-after on 429, and tell spend limits from rate limits.

    AI engineering10 min read
  2. Handling LLM API Rate Limits: Retry, Backoff and Queues in Python

    Handle 429 errors from the OpenAI API in Python with Retry-After, exponential backoff with jitter, a concurrency limiter and a queue, without retry storms.

    AI engineering9 min read
  3. LLM Model Routing: Send Easy Requests to a Cheaper Model

    Build a simple Python router that sends each request to a small or large Claude model, then measure cost and quality against an eval set before you ship it.

    AI engineering12 min read
  4. Getting Started with LLM Evals: A Small-Team Setup with Code

    Build your first LLM eval in an afternoon: write 30 test cases in a JSONL file, grade them with simple code, add an LLM grader for tone, and track the score between prompt changes.

    AI engineering9 min read
  5. Semantic Search With Azure OpenAI Embeddings in Python: A Minimal Example

    Build a small semantic search in Python with Azure OpenAI embeddings: split text into chunks, embed them in batches, and rank results by cosine similarity.

    AI engineering8 min read
  6. Claude Message Batches API: Cut Bulk Job Costs in Python

    Run large asynchronous Claude jobs at 50 percent of standard API prices with the Message Batches API: Python create, poll and results code, limits and fixes.

    AI engineering9 min read