Blog

Chat Completions or Responses: Choosing the API Shape Your Compatible Endpoint Actually Supports

The migration guide reduces the difference to three changes: the endpoint, how you read output, and how you carry state. What each difference means when your endpoint claims compatibility.

Chat Completions or Responses: Choosing the API Shape Your Compatible Endpoint Actually Supports

Two API shapes are in circulation for the same models, and for teams calling a compatible endpoint the interesting question is not which one wins on paper. It is which parts of which shape your endpoint actually implements. The official migration guide compresses the difference into three related changes: send requests to a different endpoint, read output from a different structure, and choose how your application carries state between turns. Everything else follows from those three, and "compatible" on a vendor's landing page tells you nothing about whether any of them are done faithfully.

The three changes, in the guide's own framing

The guide's summary is worth quoting because it sets the scope of the whole migration:

Treat migration as three related changes: send requests to /v1/responses, read output from a typed output array, and choose how your application will carry state between turns.

The first change is mechanical. The second is where most code breaks, because the response objects are not variations on a theme: in Chat Completions you receive an array of choices, each containing a message, while the Responses API returns a typed response object with its own id and an output array of Items. A convenience helper, output_text, extracts the text when text is all you need.

The differences, element by element

The migration guide lays out the deltas directly: Chat Completions vs Responses, summarized below without judgment, and to be verified against the official pages current when you build:

| Concern | Chat Completions | Responses |
| --- | --- | --- |
| Endpoint | POST /v1/chat/completions | POST /v1/responses |
| Output | choices, each with a message | Typed response object with output Items, plus an output_text helper |
| Multiple generations | n returns parallel generations as choices | No n; one generation per request, use separate requests for candidates |
| System guidance | A system message in the list | Top-level instructions, separate from input |
| Input | A list of messages | A string or a list of messages |
| State between turns | You store the transcript and resend messages | previous_response_id, prior output Items, or the Conversations API |
| Structured output | response_format | text.format |
| Tool calls | Function config and returned calls in one shape | Both the request config and the calls sent back differ |

Two rows deserve expansion. On state, the guide notes that store: true maintains state from turn to turn while preserving reasoning and tool context, and that storage defaults differ between the two APIs, with store: false available to disable it on either. On multiple generations, the guide is explicit: Chat Completions can return multiple parallel generations as choices using the n parameter, while the Responses API omits this parameter and returns only one generation. If your product lets a user pick among candidate drafts in a single call, that is a constraint on shape choice, not a detail.

One limitation, quoted precisely

There is a tool-calling constraint that tends to get mangled in retellings, so here it is exactly as the migration guide states it:

Starting with GPT-5.4, Chat Completions does not support tool calling with reasoning_effort values other than none.

Read that as written: it is a statement about Chat Completions starting from a specific model generation, about the combination of tool calling and non-none reasoning effort. It is not a claim that tool calling is unsupported everywhere, not a claim about every model, and not a claim about compatible endpoints at all. If your integration uses tools plus reasoning effort, this sentence determines which shape you can use on the OpenAI side, and it is worth checking the current page before you design around it.

What a compatible endpoint actually implements

Compatible endpoints usually implement Chat Completions first and vary in how much of the Responses shape they cover, which is why the only reliable answer comes from a test, not a compatibility table. The method is unglamorous: build a real payload that exercises the features you depend on, including a tool definition and a structured output field, and send it in both shapes. Then check three things.

First, is the request accepted at all. A 404 on the endpoint path, a validation error on a field name, or a silent drop of a parameter all mean the same thing for your planning, though only some of them are loud about it.

Second, does the response structure match the official one. Field names, array nesting, and, for streaming, event names. A shape that returns text under a different key than documented is a shape you will spend a week debugging at the worst possible moment.

Third, are the errors readable. Send a deliberately invalid field in each shape and see whether the error names the field or collapses into a generic failure. This is the difference between a five-minute fix and an afternoon of bisecting.

The same checklist discipline this site applies to compatible integrations in general is in the integration checklist. And run the test from the environment that will actually call the endpoint: behind a corporate proxy or TLS inspection layer, path rewriting and header handling can change what you are really talking to, as the corporate network guide describes for a related class of problems.

Choosing by constraint, not by preference

The honest way to pick a shape is to list what your product actually needs and see which shape can deliver it:

  • Need multiple candidates from one call? That is the n parameter, and per the guide it exists in Chat Completions and not in Responses. Stay on Chat Completions, or change the product interaction.
  • Need managed state with previous_response_id? That is Responses. But carry the guide's caveat with you: previous_response_id does not carry over the previous response's top-level instructions, so stable instructions must be resent on each request.
  • Need text.format structured outputs as documented? That is Responses; the Chat Completions field is response_format. The two field names are not interchangeable, and the structured output guide covers how the shapes differ across compatible providers.
  • Need maximum portability across endpoints? Use the subset both shapes support, and manage state yourself rather than depending on either API's memory.

One rule that saves a codebase: do not mix the two shapes' field names in the same client. response_format sent to Responses, or text.format sent to Chat Completions, is not a clever compatibility trick; it is a bug that will present differently on every endpoint you touch.

The migration, in three steps

Each step has an acceptance point, and the acceptance point is what prevents a half-migrated client from shipping.

Step one: the endpoint. Change the path to /v1/responses and get a response object back. Acceptance: a text-only request returns a typed response with an id, and output_text contains the expected text.

Step two: how you read output. Replace choices[0].message access with reading the output array, item by item, or output_text for the text-only case. Acceptance: your extraction function produces the same final text as before for plain text, and correctly identifies non-text items when tools or reasoning are in play. Tool calls are the sharp edge: both the function configuration on the request and the calls returned in the response have different shapes between the APIs, which the function calling guide walks through end to end; the production-side discipline for running tool loops is covered in the tool calling guide.

Step three: state. Decide explicitly between resending prior items yourself, using previous_response_id with instructions resent, or a persistent conversation object. Acceptance: a multi-turn conversation produces the same follow-up behavior as your Chat Completions baseline, including after a process restart. If you stream responses, align the event names at the same time; the event sequence differences are covered in the streaming guide.

Common failures

These recur in integrations that migrated in a hurry:

  1. Sending response_format to a Responses-shaped endpoint, and concluding the endpoint lacks structured output.
  2. Still reading choices from a response object that has output items.
  3. Assuming n still works, and silently getting one generation where the product expected several.
  4. Treating previous_response_id as "the API remembers everything," including instructions it does not carry over.
  5. Assuming the two shapes are otherwise identical, then discovering the tool-call differences in production.
  6. Reading "the documentation says it supports this" as "my endpoint supports this." The documentation describes the official API; your endpoint is a separate implementation.

One engineering reminder

API shape is not a network detail. It seeps into your type definitions, your log field names, your test fixtures, and your on-call runbooks. Decide the shape before the client is written, document which parts of it you depend on, and when a second endpoint enters the picture, run the payload test again rather than assuming the first answer carries over.

The checklist

  1. Confirm which shapes your endpoint accepts, with a real payload, not a compatibility claim.
  2. Fix one set of types and field names per shape, and do not mix them.
  3. Align structured output fields: response_format or text.format, never both in one client.
  4. Align tool definition and tool call shapes, request and response sides.
  5. Choose the state strategy and write down what it does not carry over.
  6. Align streaming event names if you stream.
  7. Send each real payload in both shapes once, and keep the results as integration tests.
  8. Run the test from the environment that will call the endpoint in production.

None of this makes either shape the right answer in general; it makes your choice an answer about your endpoint. Endpoint-level specifics for the platform this site documents live in its documentation, and every field name, parameter, and event name quoted here should be verified against the official pages current at build time.