In this post
What this post covers
How this was tested
openai-python 3.26.0 on Python 3.11. Every request body below was captured from the SDK with an httpx MockTransport, so the shapes are exact; no live model output is shown.
OpenAI has two APIs for generating text: the older Chat Completions endpoint (client.chat.completions.create) and the newer Responses endpoint (client.responses.create). Both are supported. OpenAI has said it will keep supporting Chat Completions, and Responses is where new features such as built-in tools and server-side conversation state land first.
Short answer: for a new project, start with Responses. For an existing app on Chat Completions that works, there is no deadline to migrate; move when you need built-in tools, server-side state, or a cleaner tool-calling loop.
The rest of this post is the same tasks written both ways, with the exact JSON each SDK call sends.
The simplest call
from openai import OpenAI
client = OpenAI()
# Chat Completions
cc = client.chat.completions.create(
model="gpt-5",
messages=[
{"role": "system", "content": "Be brief."},
{"role": "user", "content": "Say hi"},
],
)
print(cc.choices[0].message.content)
# Responses
r = client.responses.create(
model="gpt-5",
instructions="Be brief.",
input="Say hi",
)
print(r.output_text)
What the SDK actually sends:
POST /v1/chat/completions
{"model": "gpt-5", "messages": [{"role": "system", "content": "Be brief."}, {"role": "user", "content": "Say hi"}]}
POST /v1/responses
{"model": "gpt-5", "input": "Say hi", "instructions": "Be brief."}
Three differences show up immediately:
| Chat Completions | Responses | |
|---|---|---|
| System prompt | a system message |
instructions (a system/developer message in input also works) |
| User input | messages list |
input: a string or a list of items |
| Reading the text | choices[0].message.content |
output_text (a convenience property) |
input also accepts the familiar list of {"role": ..., "content": ...} messages, so migrating a multi-turn prompt is mostly renaming messages to input.
Output is a list of items, not choices
A Chat Completion returns choices, each with one message. A Response returns output: a list of typed items. A plain answer is one message item; a tool call is a function_call item; reasoning models can add reasoning items. output_text joins the text of all message items for you, which is why most simple code never touches output directly.
There is no n parameter in Responses. If you sampled several candidates with n=3, make three calls.
Conversation state
With Chat Completions you resend the full history on every turn. Responses can keep it on OpenAI’s side:
first = client.responses.create(model="gpt-5", input="My name is Priya.")
second = client.responses.create(
model="gpt-5",
input="What's my name?",
previous_response_id=first.id,
)
The second request body is just {"model": "gpt-5", "input": "What's my name?", "previous_response_id": "resp_..."}. Two things to know:
- Chaining needs the earlier response to be stored. Responses are stored by default; pass
store=Falsewhen you must not keep data on OpenAI’s side (and then manage history yourself, exactly as with Chat Completions). - You are still billed for the earlier turns as input tokens. Server-side state saves you bandwidth and bookkeeping, not tokens.
The SDK also exposes a conversation parameter for longer-lived conversation objects. For most apps, either previous_response_id or your own history in a database is enough.
Function calling
The tool definition is flatter in Responses:
# Chat Completions
tools = [{"type": "function", "function": {
"name": "get_order", "description": "Look up an order",
"parameters": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]}}}]
# Responses
tools = [{"type": "function",
"name": "get_order", "description": "Look up an order",
"parameters": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]}}]
When the model wants the tool, Chat Completions puts tool_calls on the assistant message and you reply with a {"role": "tool", "tool_call_id": ...} message. In Responses the model returns a function_call item with a call_id, and you send back a function_call_output item:
r = client.responses.create(model="gpt-5", input="Where is order 42?", tools=tools)
call = next(i for i in r.output if i.type == "function_call")
result = get_order(**json.loads(call.arguments))
r2 = client.responses.create(
model="gpt-5",
previous_response_id=r.id,
input=[{"type": "function_call_output", "call_id": call.call_id, "output": json.dumps(result)}],
tools=tools,
)
print(r2.output_text)
With previous_response_id you don’t resend the user message or the tool call, only the result.
Built-in tools
This is the biggest functional difference. Responses can run hosted tools inside the same call: web search, file search over vector stores, code interpreter, remote MCP servers and more. You list them in tools alongside your own functions and the model decides when to use them. Chat Completions only calls your functions; anything else you build and run yourself.
Hosted tools are billed separately from tokens, and file search stores your documents on OpenAI’s side. Check the pricing page and your data rules before turning them on.
Structured output
response_format from Chat Completions moves under text.format:
# Chat Completions
response_format={"type": "json_schema", "json_schema": {"name": "ticket", "schema": schema, "strict": True}}
# Responses
text={"format": {"type": "json_schema", "name": "ticket", "schema": schema, "strict": True}}
Both SDK surfaces also have a .parse() helper that accepts a Pydantic model and returns a parsed object.
Streaming
Chat Completions streams chunks with choices[0].delta.content. Responses streams typed events; the text arrives in events of type response.output_text.delta:
stream = client.responses.create(model="gpt-5", input="Explain MVCC in one paragraph", stream=True)
for event in stream:
if event.type == "response.output_text.delta":
print(event.delta, end="", flush=True)
Typed events make it easier to show tool progress (search started, code running) in a UI, at the cost of a slightly longer loop.
Migration checklist
- Rename
messagestoinput; move the system prompt toinstructionsif you like. - Replace
choices[0].message.contentwithoutput_text. - Flatten tool definitions (drop the nested
functionkey) and switch tool results tofunction_call_outputitems withcall_id. - Move
response_formattotext={"format": ...}. - Update streaming to handle
response.output_text.deltaevents. - Decide on state:
store=Falseplus your own history, orprevious_response_id. - Re-run your evals. Same model, different endpoint can still change outputs slightly through defaults; measure before switching production traffic.
The free lesson Responses API vs Chat Completions covers this with diagrams. If you want to go deeper on building with the SDK (tools, structured output, streaming and evals in a real project), the OpenAI SDK course starts with three free lessons.
Want the production version of this?
A production RAG + MCP starter kit for FastAPI: hybrid search, validated citations, evals in CI, Docker, 32 tests. It runs offline in about 60 seconds.
See ShipRAG or get the free RAG checklist first.