Responses API vs Chat Completions: Which to Build On
A field guide to the OpenAI Responses API and Chat Completions: request shape, output items, statefulness, reasoning, tools, and when each endpoint is the right choice.
If you are starting a new integration today, build on the Responses API (POST /v1/responses) and reserve Chat Completions (POST /v1/chat/completions) for code that already exists and works. The two endpoints solve the same core problem — send a prompt, get a model reply — but they model the conversation differently. Chat Completions treats a turn as a flat list of messages and returns a choices array. Responses treats a turn as a list of typed input items and returns a typed output array that can hold messages, reasoning, and tool calls side by side. That structural difference is the whole story, and it decides which endpoint fits your workload.
This article compares the two on request shape, output structure, state, reasoning, and tools, then gives you a decision rule you can apply without guessing. It does not deprecate anything: Chat Completions remains supported and is still the most portable shape across providers.
The request shape: messages vs input
Chat Completions expects a messages array where every element has a role (system, user, assistant, tool) and content. Everything about a turn — instructions, history, tool results — is glued into that one list.
from openai import OpenAI
client = OpenAI(base_url="https://your-gateway.example.com/v1", api_key="AVE_GATEWAY_API_KEY")
completion = client.chat.completions.create(
model="gpt-5.6",
messages=[
{"role": "system", "content": "You are a terse assistant."},
{"role": "user", "content": "Name three uses for an API gateway."},
],
)
print(completion.choices[0].message.content)
Responses splits those concerns apart. Top-level guidance moves to instructions, and the turn content moves to input, which accepts either a plain string or an array of typed items.
response = client.responses.create(
model="gpt-5.6",
instructions="You are a terse assistant.",
input="Name three uses for an API gateway.",
)
print(response.output_text)
Two things are worth noticing immediately. First, response.output_text is a convenience accessor that concatenates the text output for you, replacing the completion.choices[0].message.content path. Second, instructions is not sticky across chained turns — if you continue a conversation with previous_response_id, the prior response's instructions are not carried over, so you resend them each turn.
The output shape: choices vs output items
Chat Completions returns a choices array. With the n parameter you could ask for several candidate completions; each was a separate choice. Responses removes n and returns a single generation, but that generation is an output array of items, each with its own type. The items the model returns are:
message— assistant text (and any refusals)reasoning— reasoning summaries or encrypted reasoning tokensfunction_call— a tool call the model wants you to run
Notice what is absent: function_call_output is not a model-returned item. It is an input item you construct and send back to hand a tool result to the model, keyed by call_id. Keeping the two lists distinct — output items you read, input items you write — heads off a common source of confusion, because the call and its result live on opposite sides of the exchange.
Treating every output entry as a message is the most common migration mistake. A reasoning model may emit a reasoning item before a message, and a tool-using turn may emit a function_call with no user-visible text at all. Iterate over output by type instead of assuming index 0 is your answer:
for item in response.output:
if item.type == "message":
print(item.content[0].text)
elif item.type == "function_call":
print("tool wanted:", item.name, item.arguments)
Usage accounting also changes vocabulary. Chat Completions reports prompt_tokens and completion_tokens; Responses reports input_tokens and output_tokens on the usage object. Any dashboard that parses token counts needs to handle both shapes, so a gateway that normalizes them into one accounting view saves every client from carrying two code paths.
State: client-managed history vs server-stored threads
With Chat Completions you own the conversation: you resend the entire messages history on every call, and the request carries everything the model needs. That client-managed shape is simple to reason about and easy to route through any proxy, which is a large part of why it became the industry lingua franca. Whether OpenAI retains a copy of the exchange is a separate question from the endpoint — it is governed by the store setting and your account defaults (which can enable storage for logging), not by Chat Completions being inherently "stateless." If you must guarantee nothing is kept upstream, send store: false explicitly rather than assuming the endpoint keeps nothing.
Responses are stored by default. Each call returns an id, and you can continue the thread by passing previous_response_id on the next request instead of resending history. You have three ways to manage state:
previous_response_idto chain to a stored response,- manually passing prior
outputitems back intoinput, - the Conversations API for longer-lived threads.
Storage is a convenience, not a mandate. Set store: false to opt out, which is what you want for zero-data-retention workflows. One billing detail matters for planning: even when you chain with previous_response_id, all previous input tokens in the chain are billed as input tokens. Chaining saves you from resending bytes over the wire; it does not make earlier context free.
Reasoning: where Responses pulls ahead
The clearest reason to prefer Responses is reasoning models. Because reasoning is a first-class output item type, the API can preserve reasoning state across turns rather than discarding it between calls. For stateless setups you can request encrypted reasoning by sending store: false together with include: ["reasoning.encrypted_content"]; the API returns encrypted reasoning tokens that you pass back on the next turn, and the content is decrypted in memory and then discarded. Chat Completions has no equivalent typed slot for reasoning, so a reasoning model loses that continuity between turns.
If your product depends on multi-step reasoning, tool loops driven by a reasoning model, or long agentic runs, Responses is the endpoint designed for it. For a deeper look at the parameters involved, see reasoning model API parameters.
Tools and structured output
Both endpoints support function calling and structured output, but the field names differ:
| Concern | Chat Completions | Responses |
|---|---|---|
| Endpoint | /v1/chat/completions | /v1/responses |
| Turn content | messages[] | input (string or items) |
| System prompt | system message | instructions |
| Result text | choices[0].message.content | output_text |
| Structured output | response_format | text.format |
| Tool result linkage | tool_call_id on a tool message | function_call_output with call_id |
| Multiple candidates | n | removed (single generation) |
| Native tools | manual wiring | tools: [{ "type": "web_search" }] |
The text.format versus response_format swap is easy to miss and produces confusing errors when you send the wrong one. For Structured Outputs, set strict: true explicitly on the schema so the model is bound to it. (A separate omit-strict fallback — attempt strict enforcement, degrade to non-strict on an incompatible schema — applies to Responses function/tool definitions, not to text.format structured outputs; do not conflate the two.) If you rely on JSON Schema outputs, read structured outputs with JSON Schema before you port validation code.
How a tool loop differs in practice
The tool-calling round trip is where the two shapes feel most different, so it is worth seeing side by side. In Chat Completions, the model returns an assistant message whose tool_calls you execute, then you append a message with role: "tool" and a matching tool_call_id:
# Chat Completions: append a tool result as a message
messages.append({"role": "tool", "tool_call_id": call.id, "content": result})
In Responses, the model emits a function_call item, and you feed the result back as a function_call_output item linked by call_id — not as a message:
# Responses: append a typed function_call_output item
client.responses.create(
model="gpt-5.6",
previous_response_id=response.id,
input=[{
"type": "function_call_output",
"call_id": call.call_id,
"output": result,
}],
)
The linkage key is named call_id in Responses and tool_call_id in Chat Completions, and sending a result without the matching id is a frequent source of errors. Because Responses keeps the call and its result as distinct typed items, an agent loop reads more cleanly: you never have to unpack tool intent from inside an assistant message. Portable tool definitions across providers still need their own compatibility layer.
A decision rule you can apply
You do not need to agonize over this. Use the table:
| Situation | Endpoint |
|---|---|
| New integration, especially agents or reasoning models | Responses |
| Existing, working Chat Completions code with no new requirements | Keep Chat Completions |
| You need maximum portability across many providers today | Chat Completions (the widely mirrored shape) |
| You want server-managed conversation state or native tools | Responses |
| You want encrypted reasoning continuity with zero retention | Responses with store: false + encrypted reasoning |
A gateway makes this choice cheaper on both sides. Behind one base URL you can run Chat Completions traffic and Responses traffic to the same upstreams, register providers once, and publish what each model supports and costs on a public pricing page. If you later migrate an endpoint, the client change is small and the routing stays put. The step-by-step path is covered in migrate Chat Completions to the Responses API.
Streaming works on both, with different events
Both endpoints stream over Server-Sent Events, but the event vocabulary differs. Chat Completions streams chat.completion.chunk objects with choices[].delta and terminates with a literal data: [DONE]. Responses streams semantic events such as response.created, response.output_text.delta, and response.completed, which are easier to route because each event names what it is. If you are building a token-by-token UI, streaming LLM responses with SSE walks through parsing both formats.
FAQ
Is Chat Completions deprecated now that Responses exists?
No. Chat Completions remains a supported endpoint and is still the most widely mirrored request shape across non-OpenAI providers. The Assistants API is the piece OpenAI has marked for sunset, not Chat Completions. Build new agentic work on Responses because of its structured output items and reasoning support, not because Chat Completions is going away.
Can I get multiple candidate answers from the Responses API like n in Chat Completions?
Not with a single n parameter — Responses returns one generation per call. If you need several candidates, send multiple requests. This is a genuine behavior change to account for if your Chat Completions code relied on n > 1.
Does previous_response_id reduce my token bill?
It reduces bytes on the wire because you stop resending history, but it does not reduce billed input tokens: all previous input tokens in a chain are billed as input tokens. Treat chaining as an ergonomics and latency feature, not a cost optimization. To actually cut context cost, look at prompt caching across OpenAI, Claude, and Gemini.
Which endpoint should a multi-provider setup standardize on?
If you route to many providers, Chat Completions is still the most portable common denominator today, while Responses is the richer OpenAI-native surface. A gateway lets you keep both: expose the shape each client already speaks and translate per channel.
Run both against one workflow
Pick one low-risk workflow and run it twice through your gateway — once as a Chat Completions call and once as a Responses call — then compare the output/choices shape, token accounting, and latency in your gateway's usage logs. That single side-by-side tells you more than any spec summary about which endpoint fits your code.