OpenAI Responses API: What It Is vs. Chat Completions
Learn what the OpenAI Responses API (/v1/responses) is and how it compares to Chat Completions (/v1/chat/completions) for schemas, tools, SSE, and Codex.
UniRoute ·
The OpenAI Responses API (POST /v1/responses) is OpenAI's item-based interface for generating model responses from text and image inputs and returning typed text or JSON output[] items (reasoning, message, function_call, and tool outputs). Per OpenAI's Responses API overview and official migration guide, it supports stateful multi-turn interactions, built-in tools, and custom function calling. Chat Completions (POST /v1/chat/completions) remains fully supported, while OpenAI recommends the Responses API for new projects, agentic workflows that interleave reasoning with tool execution, and Responses-native clients such as the official Codex CLI.
If your existing service already runs reliably on /v1/chat/completions with standard message arrays and custom function calls, you do not need to rewrite it immediately. You should adopt /v1/responses when you are building a new agent loop, using clients that require the Responses wire protocol, or relying on model features where OpenAI restricts Chat Completions (for example, tool calling with non-none reasoning effort on newer GPT-5.4+ specifications).
Before diving into payload diffs, keep three OpenAI endpoint generations distinct:
- Legacy Completions (
POST /v1/completions): OpenAI's early single-prompt text completion endpoint (promptstring in, text completion out). Do not confuse this legacy endpoint with Chat Completions. - Chat Completions (
POST /v1/chat/completions): The widely adoptedmessages[]array interface (system,developer,user,assistant, andtoolroles) returningchoices[].message. It serves as a common compatibility baseline across SDKs and gateways; verify each target provider. - Responses API (
POST /v1/responses): OpenAI's item-based agentic interface accepting a string or typedinputitem array (plus optional top-levelinstructions) and returning a polymorphicoutput[]list of items (reasoning,message,function_call, and tool outputs).
30-Second Endpoint Selection Guide
| Scenario or Workload | Recommended Endpoint | Why It Fits |
|---|---|---|
Existing production app using messages[] and standard chat or JSON schemas | POST /v1/chat/completions | Stable, supported by OpenAI and UniRoute OpenAI-compatible routes without rewriting request builders or SSE parsers |
| Multi-provider app switching across non-OpenAI models with minimal adapter code | POST /v1/chat/completions | A common compatibility baseline around messages[] and choices[0].message; verify each target provider |
| New OpenAI-targeted agent loop combining reasoning turns and function calling | POST /v1/responses | Preserves typed reasoning and function_call items across turns; avoids OpenAI's GPT-5.4+ Chat Completions restriction on tool calls when reasoning_effort is not none |
| Connecting the official OpenAI Codex CLI or IDE extension | POST /v1/responses | Codex model_providers.<id>.wire_api only accepts "responses" (and defaults to "responses") |
Calling GPT models through UniRoute (https://api.uniroute.app/v1) | Either endpoint, verified per model and route | UniRoute publishes both /v1/chat/completions and /v1/responses; check the UniRoute model catalog and test your key's active route before production cutover |
1. Core Schema Comparison: /v1/responses vs. /v1/chat/completions vs. /v1/completions
While both /v1/chat/completions and /v1/responses accept a model ID and generate model output, their request bodies, output containers, tool schemas, and streaming events are structurally different. You cannot point an existing Chat Completions parser at /v1/responses by changing the URL path alone.
| Dimension | Responses API (POST /v1/responses) | Chat Completions (POST /v1/chat/completions) | Legacy Completions (POST /v1/completions) |
|---|---|---|---|
| Primary input | input (string or array of typed Items) + optional top-level instructions | messages[] array of role-tagged message objects | Single prompt string or token array |
| Primary output | output[] array of typed Items (reasoning, message, function_call, etc.); no n parameter | choices[] array (choices[0].message.content, tool_calls); supports n candidates | choices[].text string completion |
| Plain-text extraction | response.output_text helper in official OpenAI SDKs; raw HTTP callers must filter output[] for type === "message" | response.choices[0].message.content in both SDKs and raw HTTP JSON | response.choices[0].text |
| Multi-turn context | Portable baseline: append prior output items to next input[]; OpenAI official also supports previous_response_id / Conversations | Portable baseline: send full messages[] history on every turn | Manual prompt concatenation |
| OpenAI default storage | Stored by default on OpenAI (store: true); set store: false to opt out (ZDR uses encrypted_content) | Stored by default for new OpenAI accounts; set store: false to opt out | Legacy behavior |
| Function tool definition | Internally tagged: { "type": "function", "name": "...", "parameters": {...} }; strict mode attempted by default | Externally tagged: { "type": "function", "function": { "name": "...", "parameters": {...} } }; non-strict by default | Not supported |
| Tool call & result correlation | Separate function_call and function_call_output Items linked by call_id | Assistant message.tool_calls[] paired with role: "tool" messages via tool_call_id | Not supported |
| Structured Outputs | text: { format: { type: "json_schema", name, strict, schema } } | response_format: { type: "json_schema", json_schema: { name, strict, schema } } | Not supported |
Streaming (stream: true) | Semantic SSE lifecycle events (response.output_text.delta, response.completed, response.incomplete, etc.) | Chunk deltas (choices[].delta.content, choices[].delta.tool_calls) | Token text chunks |
Why response.output_text Differs from Raw HTTP JSON
One of the most common migration bugs happens when developers switch from the official OpenAI Python or TypeScript SDK to raw HTTP fetch calls:
- In official OpenAI SDKs:
response.output_textis a client-side SDK convenience property that walks theoutput[]array and concatenates all text segments where an output item hastype: "message"and itscontent[]entry hastype: "output_text". - In raw HTTP responses: The JSON payload returned by
POST /v1/responsesis not guaranteed to include a top-leveloutput_textfield. Furthermore, you should never hardcoderesponse.output[0].content[0].text—on reasoning models or tool-using turns,output[0]is frequently areasoningitem or afunction_callitem rather than a user-facingmessage. Always iterate overoutput[], filter foritem.type === "message", and readcontent[]blocks wherepart.type === "output_text".
2. Illustrative Python SDK Code Comparison
The Python examples below are illustrative request and response structure templates based on OpenAI's official migration specification. No live paid inference was executed to generate these snippets; set MODEL_ID in your environment to an exact model identifier verified for your account and target route.
A. Chat Completions (POST /v1/chat/completions — OpenAI Official Schema)
import os
from openai import OpenAI
# Illustrative template using OpenAI's default first-party endpoint.
# Keep OPENAI_API_KEY paired strictly with OpenAI's official service.
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"], max_retries=0)
MODEL_ID = os.environ["MODEL_ID"] # Set to a verified model ID for your account
completion = client.chat.completions.create(
model=MODEL_ID,
messages=[
{"role": "developer", "content": "Return concise backend architecture advice."},
{"role": "user", "content": "When should a service keep Chat Completions?"},
],
)
print(completion.choices[0].message.content)
B. Responses API (POST /v1/responses — OpenAI Official Schema + SDK vs. Raw JSON Parsing)
import os
from openai import OpenAI
# Illustrative template using OpenAI's default first-party endpoint.
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"], max_retries=0)
MODEL_ID = os.environ["MODEL_ID"] # Set to a verified model ID for your account
response = client.responses.create(
model=MODEL_ID,
instructions="Return concise backend architecture advice.",
input=[
{
"role": "user",
"content": "When should a new agent workflow use the Responses API?",
}
],
)
# 1. Official OpenAI SDK convenience helper:
print(response.output_text)
# 2. Traversing typed SDK output item objects (Pydantic/SDK models):
sdk_text_segments = []
for item in response.output:
if getattr(item, "type", None) == "message":
for part in getattr(item, "content", []):
if getattr(part, "type", None) == "output_text":
sdk_text_segments.append(part.text)
print("".join(sdk_text_segments))
# 3. Traversing a raw HTTP JSON dictionary (e.g. from requests/httpx response.json()):
raw_payload = response.model_dump()
dict_text_segments = [
part.get("text", "")
for item in raw_payload.get("output", [])
if item.get("type") == "message"
for part in item.get("content", [])
if part.get("type") == "output_text"
]
print("".join(dict_text_segments))
C. Portable Multi-Turn Context Replay (input List Append for Text Turns)
Whether you call OpenAI directly or route through an OpenAI-compatible gateway, manually appending the previous turn's response.output items to your next input array is the most portable way to preserve multi-turn context across text turns:
# Illustrative text-turn context replay without relying on server-side state.
# (If a turn emits function_call items, your code must also execute them and append
# matching function_call_output items before sending the next request.)
conversation_input = [
{"role": "user", "content": "Summarize the difference between messages[] and input[]."}
]
first_turn = client.responses.create(
model=MODEL_ID,
instructions="Answer in two short bullet points.",
input=conversation_input,
)
# Append prior output items (such as reasoning and assistant message items)
conversation_input.extend(first_turn.output)
conversation_input.append(
{"role": "user", "content": "Now give a one-sentence migration takeaway."}
)
second_turn = client.responses.create(
model=MODEL_ID,
instructions="Answer in two short bullet points.",
input=conversation_input,
)
D. Pointing the OpenAI SDK to UniRoute (Separate Gateway Configuration)
When routing requests through UniRoute, configure base_url="https://api.uniroute.app/v1" and authenticate exclusively with your UNIROUTE_API_KEY. Never send an OpenAI Platform key to UniRoute or a UniRoute key to OpenAI, and never pass API keys in CLI arguments or public config files:
import os
from openai import OpenAI
# Separate gateway configuration for UniRoute.
# Verify that your chosen MODEL_ID and active route support the target endpoint.
uniroute_client = OpenAI(
api_key=os.environ["UNIROUTE_API_KEY"],
base_url="https://api.uniroute.app/v1",
max_retries=0,
)
MODEL_ID = os.environ["MODEL_ID"] # Set to a model ID verified in /models for your key
# Call uniroute_client.chat.completions.create(...) or uniroute_client.responses.create(...)
# depending on what your target model and UniRoute route support.
3. Multi-Turn State, previous_response_id Billing, and Data Storage (store)
A common misconception is that switching to /v1/responses or passing previous_response_id automatically cuts token costs by avoiding history transmission. In practice, you should separate network payload convenience from token billing rules and gateway state boundaries.
How previous_response_id Works—and How It Is Billed on OpenAI
In OpenAI's official Responses specification, passing previous_response_id lets you chain a follow-up turn to a stored response without resending the full conversation array over the wire. Keep two official rules in mind:
- Prior context tokens remain billable input tokens: When you pass
previous_response_id, OpenAI states that all prior input tokens in the response chain are still billed as input tokens on the new turn. Using/v1/responsesorprevious_response_iddoes not change per-million-token model prices or exempt conversation history from input billing; any cost reduction on repeated prefixes still depends on prompt caching rules. For a detailed breakdown of ordinary uncached input, cache reads, cache writes, and reasoning output tokens, see the OpenAI API Price Guide: GPT Token Costs on UniRoute. - Top-level
instructionsdo not carry over automatically: According to OpenAI's documentation, system or developer instructions passed via the top-levelinstructionsparameter apply only to that response. If you chain turns withprevious_response_idand still need those instructions, you must passinstructionsagain on the new request.
store: true, store: false, and Zero Data Retention (ZDR)
In OpenAI's first-party environment:
- Default storage: Responses are stored by default (
store: true), and Chat Completions are also stored by default for new OpenAI accounts so they appear in evaluation and dashboard logs. - Opting out (
store: false): You can setstore: falseon either endpoint to disable server-side response storage. When using reasoning models on OpenAI withstore: falseor Zero Data Retention (ZDR), OpenAI supports requestinginclude: ["reasoning.encrypted_content"]so your client can replayencrypted_contentitems manually on the next turn without server-side state. - Gateway state boundary: Stateful capabilities such as
previous_response_id, the Conversations API,GET /v1/responses/{response_id},DELETE /v1/responses/{response_id},store,encrypted_content, and background mode (background: true) are OpenAI first-party specifications. When calling models through UniRoute or any multi-channel gateway, do not assume server-side response storage orprevious_response_idchaining is available across routes—use manualinput[]ormessages[]history replay unless you have verified stateful support on your specific route.
4. Function Calling, Structured Outputs, and Streaming SSE Differences
If your application uses tool calling, enforced JSON schemas, or real-time token streaming, migrating between /v1/chat/completions and /v1/responses requires updating both your request serializers and your response consumers.
Function Calling Schema and Strict Mode Defaults
In Chat Completions, function declarations are externally tagged inside a function wrapper object and default to non-strict parameter validation unless you explicitly set "strict": true. In the Responses API, function declarations are internally tagged at the top level of each tool object and attempt strict mode by default:
# 1. Chat Completions tool definition (externally tagged; non-strict unless specified)
chat_tools = [
{
"type": "function",
"function": {
"name": "get_route_status",
"description": "Check latency status for a model route",
"strict": True,
"parameters": {
"type": "object",
"properties": {"model_id": {"type": "string"}},
"required": ["model_id"],
"additionalProperties": False,
},
},
}
]
# 2. Responses API tool definition (internally tagged; strict attempted by default)
responses_tools = [
{
"type": "function",
"name": "get_route_status",
"description": "Check latency status for a model route",
"parameters": {
"type": "object",
"properties": {"model_id": {"type": "string"}},
"required": ["model_id"],
"additionalProperties": False,
},
}
]
When a model invokes a function in the Responses API:
- The response's
output[]array contains a top-level item with"type": "function_call","name","arguments", and"call_id". - Your application executes the function locally and appends a
"type": "function_call_output"item referencing the same"call_id"to the next request'sinput[]array (alongside the priorresponse.outputitems, including anyreasoningitems emitted before the tool call). - Reasoning + tool calling on newer models: Note that in OpenAI's official documentation, starting with GPT-5.4, Chat Completions does not support tool calling with
reasoning_effortvalues other thannone. Workloads that combine active reasoning effort with tool loops should target/v1/responses. - Custom functions vs. built-in tool types and remote MCP: Beyond developer-defined
functionschemas, the Responses API specification defines built-in tool types (such asweb_search,file_search,code_interpreter,image_generation, andcomputer_use, where computer actions are executed by your client environment) as well as remote MCP server connections. Availability and billing depend on the specific tool, model, and route—using built-in tools may incur additional tool-specific fees under OpenAI's official pricing rules, and you should not assume built-in tools or remote MCP connections are supported on every gateway route.
Structured Outputs (response_format vs. text.format)
If you enforce JSON Schema output, move the schema definition from response_format (Chat Completions) into text.format (Responses API):
# Chat Completions Structured Outputs
chat_response_format = {
"type": "json_schema",
"json_schema": {
"name": "route_check",
"strict": True,
"schema": {
"type": "object",
"properties": {"endpoint": {"type": "string"}, "ready": {"type": "boolean"}},
"required": ["endpoint", "ready"],
"additionalProperties": False,
},
},
}
# Responses API Structured Outputs
responses_text_config = {
"format": {
"type": "json_schema",
"name": "route_check",
"strict": True,
"schema": {
"type": "object",
"properties": {"endpoint": {"type": "string"}, "ready": {"type": "boolean"}},
"required": ["endpoint", "ready"],
"additionalProperties": False,
},
}
}
Streaming SSE Event Consumers (choices[].delta vs. Semantic Events)
Both endpoints stream over HTTP Server-Sent Events (stream: true), but as documented in OpenAI's Streaming Responses guide, the event contracts are completely different:
- Chat Completions emits chunk objects where incremental text lives at
chunk.choices[0].delta.contentand tool argument fragments arrive inchunk.choices[0].delta.tool_calls. - Responses API emits typed lifecycle events with an explicit
event.typediscriminator:response.createdandresponse.in_progressresponse.output_item.addedandresponse.content_part.addedresponse.output_text.delta(incremental text inevent.delta) andresponse.output_text.doneresponse.function_call_arguments.deltaandresponse.function_call_arguments.done- Terminal events:
response.completed,response.incomplete,response.failed, anderror
# Illustrative Responses API SSE consumer (cannot reuse a choices[].delta loop).
# Tracks terminal completion explicitly and does not auto-retry interrupted streams.
stream = client.responses.create(
model=MODEL_ID,
input="List three checks before switching an API endpoint.",
stream=True,
)
completed = False
for event in stream:
if event.type == "response.output_text.delta":
print(event.delta, end="", flush=True)
elif event.type == "response.completed":
completed = True
print("\nStream completed:", event.response.id)
elif event.type in ("response.incomplete", "response.failed", "error"):
raise RuntimeError(f"Stream terminated without full completion: {event.type}")
if not completed:
raise RuntimeError(
"Stream ended before response.completed; inspect usage before retrying."
)
Always require response.completed before treating a streamed response as finished, and reject response.incomplete, response.failed, error, or an early EOF that ends the iterator without response.completed. Receiving early response.output_text.delta events only proves that partial tokens arrived over the stream—it does not guarantee that the turn completed cleanly or define final account settlement.
5. Codex wire_api Requirements and Verifying Routes on UniRoute
Understanding the protocol split between /v1/responses and /v1/chat/completions also explains why certain developer tools require specific endpoints.
Why Official Codex Requires /v1/responses
According to OpenAI's Codex configuration reference, custom model providers in ~/.codex/config.toml configure their wire protocol via model_providers.<id>.wire_api. In the current specification:
wire_apionly accepts"responses"(and defaults to"responses"when omitted). Codex sends typed Responses items rather than Chat Completionsmessages[], so any provider wired to Codex must expose an HTTP/v1/responsesendpoint.supports_websockets(true/false) is a separate boolean setting onmodel_providers.<id>for OpenAI's WebSocket transport mode. SupportingPOST /v1/responsesover HTTP or SSE does not automatically mean a provider or gateway route supports WebSocket sessions.
For a complete ~/.codex/config.toml template, environment variable setup, and rollback steps, read Connect Codex to UniRoute: API Key and Setup Tutorial.
How to Verify Endpoints and Roll Back Safely on UniRoute
UniRoute's published OpenAI-compatible gateway surface (https://api.uniroute.app, SDK base https://api.uniroute.app/v1) includes POST /v1/chat/completions, POST /v1/responses, and GET /v1/models. Because UniRoute routes traffic across multiple upstream channels and model providers, always keep three verification layers distinct:
- OpenAI's first-party specification: Defines protocol schemas and OpenAI first-party features on
developers.openai.com. - UniRoute's published gateway surface, model catalog, and top-up options: Browsing the UniRoute model catalog shows per-model reference and UniRoute rates (and
GET /v1/modelsconfirms model discovery), while UniRoute USD top-up and recharge options lets you prepare an account balance for testing. - Your API key's live route capability: Neither
/v1/modelsdiscovery nor the existence of/v1/responsesguarantees that every listed model or channel supports every Responses parameter, stateful storage (previous_response_id,store), built-in tools, or WebSockets. Check route selection and channel rules for your workload.
Use this checklist when adopting /v1/responses or rolling back to /v1/chat/completions on UniRoute:
- Create a dedicated test API key: Follow the API key creation guide or open API keys in the UniRoute console to create and name a separate key for your test service (where you can also rename, toggle, or delete keys as needed). Keep a small test balance for verification and store the secret in an environment variable (
UNIROUTE_API_KEY). - Send one small, non-sensitive test call with retries disabled: Set
MODEL_IDto your target model string and send a single test call to/v1/responsesor/v1/chat/completionswith client auto-retries disabled. If a request fails due to an unsupported parameter or schema mismatch, stop and fix the payload before retrying so automated client retries or agent loops do not generate repeated billable calls. - Keep endpoint path, payload schema, and stream parser paired on rollback: If you roll a service back from
/v1/responsesto/v1/chat/completions(or vice versa), switch the endpoint path, input/output serializer (input+instructions+text.format↔messages+response_format), tool definitions (internally tagged ↔ externally tagged), and SSE stream listener together as a single atomic change. - Verify your request in the UniRoute console: Open your UniRoute usage records and inspect the request detail drawer to confirm the timestamp, model ID, displayed channel classification (such as Official, Cloud vendor, or Unknown, and routing group, when shown), request status, token breakdown, and recorded cost.
- Treat usage rows and wallet settlement separately: A single usage row or diagnostic summary shows recorded request metadata rather than a full wallet reconciliation statement, and a failed or interrupted call is not automatically zero-cost. Treat any unconfirmed settlement state as unknown until verified in your authenticated account records or with private support.
- Share diagnostic summaries only with private support: If you use Copy diagnostic summary in the request detail drawer, review the text before sending. Although it excludes prompts, API keys, cookies, and account balance, it still contains private request metadata (timestamps, model ID, request ID, status codes, and recorded cost) and should never be posted in public forums or issue trackers.

