# 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.

Source: https://www.epifive.com/blog/openai-responses-api-migrate-chat-completions · Published 2026-10-29 · EpiFive

## Key takeaways

- The Responses API is the recommended API for new OpenAI projects, while Chat Completions remains supported, so you can migrate one user flow at a time.
- The main request changes are the endpoint (/v1/responses), a top-level instructions parameter instead of a system message, and input instead of messages.
- Output is an array of typed items rather than choices, so read output_text for plain text or branch on each item's type.
- Function definitions drop the nested function key, and each result must be sent back as a function_call_output with the matching call_id.
- Structured outputs move from response_format to text.format, and streaming changes from delta chunks to typed events.

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](https://www.epifive.com/blog/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

| 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:

```python
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:

```python
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`:

```python
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:

```python
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:

```python
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:

```python
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`:

```python
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`:

```python
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:

| 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`; `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

- [Migrating from Chat Completions to Responses - OpenAI API documentation](https://developers.openai.com/api/docs/guides/migrate-to-responses)

## Frequently asked questions

### 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.

