Responses API vs Chat Completions: Why Migrate
On this page
Responses API vs Chat Completions
The Chat Completions API served as the standard for years, and much of the code you find online still uses it. However, since 2025, OpenAI has designated it as legacy and recommends the Responses API for all new projects. This lesson compares the two interfaces, focusing on differences that actually impact your code.
Fundamental Changes
With Chat Completions, you built a list of messages and sent the entire list with every call. The conversation state lived in your application, never on the server:
from openai import OpenAI
client = OpenAI()
# Old model — Chat Completions (legacy)
response = client.chat.completions.create(
model="gpt-5.6-terra",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "What is the capital of France?"},
]
)
print(response.choices[0].message.content)
# Result: "The capital of France is Paris."
The Responses API reduces this same call to two parameters and provides direct access to the generated text. The system message becomes the instructions parameter, and extracting content no longer requires navigating choices[0].message.content:
from openai import OpenAI
client = OpenAI()
# New model — Responses API
response = client.responses.create(
model="gpt-5.6-terra",
input="What is the capital of France?"
)
print(response.output_text)
# Result: "The capital of France is Paris."
Server-Side Session Management
This offers the most immediate benefit. The API maintains conversation state, so you only need to reference the previous response by its ID. Imagine a support assistant: by the tenth turn, the Chat Completions approach forced you to resend the previous nine exchanges, meaning you paid for those tokens again. Here, a single field suffices:
# First question
response = client.responses.create(
model="gpt-5.6-terra",
input="My name is Marie."
)
# Second question — context is preserved automatically
followup = client.responses.create(
model="gpt-5.6-terra",
input="What is my name?",
previous_response_id=response.id
)
print(followup.output_text)
# Result: "Your name is Marie."
Guaranteed JSON, Not Hopeful JSON
The second major improvement concerns structured output. You describe the expected format, and the Responses API guarantees the response adheres to your JSON schema—allowing you to deserialize without safety nets. A registration form extracting information from free text no longer breaks just because the model decides to add an introductory sentence before its JSON:
from pydantic import BaseModel
class City(BaseModel):
name: str
country: str
population: int
response = client.responses.create(
model="gpt-5.6-terra",
input="Give me information about Paris.",
text={"format": {"type": "json_schema", "json_schema": {
"name": "city",
"strict": True,
"schema": City.model_json_schema()
}}}
)
print(response.output_text)
# Result: {"name": "Paris", "country": "France", "population": 2161000}
Function Calling and Streaming
Tool declaration follows the same simplification logic: the function descriptor is declared flatly in tools, and the call requested by the model appears directly in response.output, without extra nesting levels.
response = client.responses.create(
model="gpt-5.6-terra",
input="What is the weather like in Lyon?",
tools=[{
"type": "function",
"name": "get_meteo",
"description": "Get the weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "Name of the city"}
},
"required": ["city"]
}
}]
)
# The API returns the function call to execute directly
for item in response.output:
if item.type == "function_call":
print(f"Function: {item.name}, Arguments: {item.arguments}")
# Result: Function: get_meteo, Arguments: {"city": "Lyon"}
Streaming no longer returns a stream of undifferentiated chunks but typed events. You filter on event.type and know precisely what you are handling—a text fragment, a function argument, or the end of generation:
stream = client.responses.create(
model="gpt-5.6-terra",
input="Tell a short story.",
stream=True
)
for event in stream:
if event.type == "response.output_text.delta":
print(event.delta, end="", flush=True)
Quick Migration Guide
| Chat Completions | Responses API |
|---|---|
client.chat.completions.create() | client.responses.create() |
messages=[...] | input="..." or input=[...] |
response.choices[0].message.content | response.output_text |
| Manual history | previous_response_id |
response_format | text={"format": {...}} |
tools + tool_choice | tools (simplified) |
Should You Migrate Immediately?
The Chat Completions API remains functional and is not being removed. Existing code that runs, requires no changes, and relies on a test pipeline built around the old format gains nothing from an urgent rewrite. Similarly, if you depend on a third-party library that does not yet support the Responses API, wait for its update. However, for any new project, the answer is clear: the Responses API is the starting point, and the rest of this course proceeds from that assumption.
Key Takeaways
- The Responses API replaces Chat Completions with a simpler interface
- Server-side session management eliminates the need to resend history
- Structured Output and function calling are natively integrated
- Migration is straightforward: concepts remain the same, only syntax changes