Responses API vs Chat Completions: Differences and a Practical Migration Guide (2026)
Responses API vs Chat Completions: Differences and a Practical Migration Guide (2026)
OpenAI recommends the Responses API for all new projects while stating clearly that Chat Completions remains supported. The two endpoints will coexist, but they differ in field structure, state management, and the tool system. This guide breaks the differences into four dimensions, shows the exact code to change when migrating, and ends with a per-scenario decision table — every example runs as-is against EXAIV's endpoints.
Definition: The Responses API is OpenAI's unified interface introduced after Chat Completions: input goes in an input array, output is a typed list of output items, conversation state is stored server-side by default (store), and hosted tools like web search, file search, and code interpreter are built in. Chat Completions is a stateless protocol: each request carries the full messages history and your client owns the context.
The core differences in one table
| Dimension | Chat Completions | Responses API |
|---|---|---|
| Endpoint | /v1/chat/completions | /v1/responses |
| Input field | messages array | input (string or message array) |
| Output shape | choices[0].message.content | typed output items, read output_text |
| State | stateless; client resends full history | store: true by default; previous_response_id chains turns |
| Hosted tools | none built in | web search, file search, code interpreter, computer use, remote MCP |
| Function definitions | legacy tools format | new format — similar fields, not identical |
| Multiple candidates | supports n > 1 | removed; single result only |
Official positioning
OpenAI's own guidance: "Chat Completions remains supported; Responses is recommended for all new projects." There is no forced-migration deadline, but new capabilities — hosted tools, reasoning-state continuity — land on the Responses side only. One reference point: the Assistants API was shut down on August 26, 2026, so the convergence on Responses is deliberate.
Code comparison: same task, two shapes
Chat Completions (EXAIV endpoint /api/ai/v1/chat/completions):
curl https://exaiv.com/api/ai/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"model": "gpt-6-sol",
"messages": [
{ "role": "user", "content": "Explain what an AI gateway is in one sentence." }
]
}'
Responses API (EXAIV endpoint /api/ai/v1/responses):
curl https://exaiv.com/api/ai/v1/responses \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"model": "gpt-6-sol",
"input": "Explain what an AI gateway is in one sentence."
}'
At the SDK level: client.chat.completions.create(...) becomes client.responses.create(...), and reading the result changes from completion.choices[0].message.content to response.output_text. Full parameter references: Chat Completions and Responses.
Migrating in four steps
messages→input: plain-text requests pass a string; requests with history rename themessagesarray toinput, keeping role/content structure.- Change the result path:
choices[0].message.content→ iterateoutputfiltering fortype === "message", or use the SDK'soutput_textconvenience field. - Move system prompts to
instructions: the Chatrole: "system"message has a dedicated top-level parameter in Responses. - Rewrite tool definitions: the two function-definition formats are similar but not identical — verify field by field. Responses' hosted tools (web_search etc.) are enabled by name, no orchestration loop required.
Streaming events differ
Both endpoints stream via SSE, but the event types are completely different (Chat emits generic deltas; Responses emits named events like response.output_text.delta). Your SSE parser needs its own changes during migration.
Which one to use, by scenario
| Scenario | Recommendation | Why |
|---|---|---|
| Existing code works fine | Stay on Chat Completions | No sunset date; migrate flow by flow at your own pace |
| New project, multi-step agent, hosted tools | Responses | Hosted tools + server-side state remove a layer of orchestration code |
| Code must run across many model providers | Chat Completions | It is the industry's de-facto standard with the widest compatibility |
| Multiple candidates per request (n > 1) | Chat Completions | Responses removed the parameter |
EXAIV offers both endpoints under the same API key, so you don't have to pick a side permanently: keep existing traffic on Chat Completions, build new features on Responses, and switch underlying models from the models page.
Why two interfaces exist
The evolution context helps the decision. Chat Completions was OpenAI's early interface; because of its simple shape and provider-agnostic structure it became the de-facto standard the whole industry copied — nearly every model vendor today serves a messages-style compatible endpoint. The Responses API is OpenAI's redesign for the agent era: "the model may need to do several things" (call tools, search, run code) is a first-class concept built into the interface layer, with state management and tool execution moved back to the server. The two target different scenarios — which is exactly why they coexist.
Parameter mapping quick reference
The correspondences you'll reach for most when migrating:
| Chat Completions | Responses API | Notes |
|---|---|---|
messages | input | Plain text can be a bare string |
role: "system" | instructions | System prompt has its own parameter |
choices[0].message.content | output_text / iterate output | Different result path |
max_tokens | max_output_tokens | New name, similar meaning |
response_format | text.format | Structured output, shape changed |
tools + tool_choice | New tools format | Similar roles, verify field by field |
n (multiple candidates) | Removed | Keep Chat Completions if you need it |
| — | store / previous_response_id | State capabilities unique to Responses |
Exact fields live in the reference docs; verify one by one during migration.
State management in depth: store and previous_response_id
This is the deepest philosophical difference between the generations. Chat Completions assumes "the server remembers nothing": every turn you resend the full history, and context-window management and cost control live in your client. The Responses API stores the conversation server-side by default (store: true); the next turn only references previous_response_id, and the server continues with the full conversation and tool context intact.
Practical consequences:
- Less code: modules that maintained the messages array, trimmed history, and compacted context can be deleted
- Fewer tokens: the server reuses existing context; the cost of resending disappears
- Mind compliance: conversation data lives server-side; teams with data-residency requirements can set
store: falseat the cost of returning to full-resend mode
Five common migration pitfalls
- Empty results: the result path wasn't changed — content now lives in
output, notchoices[0].message.content - Tool calls silently failing: tool definition formats differ between generations; copying fields verbatim fails quietly — verify each one
- Broken streaming: SSE event types are completely different;
deltalogic does not map ontoresponse.output_text.delta - Errors on multiple candidates:
nis removed in Responses; multi-candidate logic needs a rewrite or should stay on Chat - State leaks:
storedefaults to true, so test data gets persisted — data-sensitive scenarios should disable it explicitly
FAQ
Streaming migration, event by event
The event names are where most migration time actually goes. The mapping at a glance:
| What happens | Chat Completions event | Responses event |
|---|---|---|
| A chunk of answer text arrives | chat.completion.chunk (delta) | response.output_text.delta |
| The model wants to call a tool | tool_calls delta inside the chunk | response.output_item.added (type: function_call) |
| A tool returns its result | you append a message, re-request | function_call_output item, continue the same call |
| The stream ends | data: [DONE] | response.completed |
Two structural differences to internalize: Chat deltas are anonymous fragments you assemble yourself, while Responses events name what they carry; and in Responses, a multi-tool turn is one request lifecycle rather than several request/response round-trips. Your SSE parser, retry logic, and timeout handling all need to be rewritten against the new event names — this is the part of the migration that always costs more than estimated.
Using Responses through a gateway
Three things worth knowing when the endpoint is proxied:
- Hosted tools execute upstream: web search, code interpreter and friends run on OpenAI's side; the gateway forwards the request and streams the result back untouched
- Server-side state stays upstream:
previous_response_idreferences stored state on OpenAI's side; the gateway transparently relays it - Your key and billing are the gateway's: one EXAIV key works for both endpoints, usage is billed per model and token regardless of endpoint — check pricing for live rates
The pre-migration checklist
Five questions, in order — stop at the first "no" and fix that layer first:
- Does a plain-text call work on the new endpoint (no tools, no streaming)?
- Does the result-reading path return text (not
undefined)? - Do tool definitions parse without schema errors?
- Does one full tool round-trip complete (call → execute → continue)?
- Does streaming render end-to-end with the new event names?
A realistic migration timeline
For a mid-sized codebase with tools and streaming, here is what a calm, three-day migration actually looks like:
Day one — text only. Point a copy of one flow at the new endpoint. messages becomes input, the result path becomes output_text. Run your existing test suite. Most teams find plain-text calls pass with no surprises; the goal today is only to prove the transport works.
Day two — tools. Rewrite one tool definition in the new format and run the full round-trip: model requests the call, you execute it, you continue the same turn. Expect to spend most of the day here — this is the field-mapping work the tables above warn about. Verify tool errors surface correctly, not silently.
Day three — streaming. Swap the SSE parser over to the named events. Render end-to-end in your real UI. Retries and timeouts get adjusted against live behavior, not assumptions.
What this timeline deliberately avoids: migrating every flow at once, rewriting orchestration that could stay on Chat Completions for another quarter, and testing streaming last on a Friday. Traffic can shift flow by flow — via a gateway, per request — while both endpoints run in parallel.