Migrating from Chat Completions to the Responses API: A Minimal Python Guide
OpenAI introduced the Responses API in March 2025. Migration is not an endpoint rename. The response has a different shape: input can begin as a simple string, output is a sequence of typed items, the Python SDK offers output_text as a convenience for collected text, and multi-turn state requires an explicit choice between a service-side response chain and application-managed history.
Freeze the behavior being migrated
Before changing code, preserve a representative evaluation set for the Chat Completions path: normal answers, refusals, empty output, timeouts, and oversized input. Record the model snapshot or configured stable name, instructions, sampling settings where supported, output limit, and error translation. Do not change the model, rewrite the prompt, introduce tools, and switch API in one release. A changed result would have no attributable cause.
A minimal Python request can remain small:
import os
from openai import OpenAI
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
response = client.responses.create(
model=os.environ.get("OPENAI_MODEL", "gpt-5-mini"),
instructions="Answer concisely and state uncertainty.",
input="Explain why idempotency matters in one paragraph.",
)
print(response.output_text)
The key comes from the environment and never belongs in source or an article. The example defaults to gpt-5-mini, but a production application should configure a model it has evaluated. Automated validation parses the code and does not make a paid request.
Do not assume output is one text field
A Chat Completions integration often reads choices[0].message.content. A Response output can contain different item types. output_text aggregates text and is convenient for a small command-line program. An audited workflow, streaming interface, or tool loop should process items by type. Preserve or report an unknown type rather than coercing a fixed array index.
Define an application result such as Answer(text, response_id, usage, status). Keep the OpenAI SDK object inside an adapter so tests use ordinary values and SDK changes do not spread into routes, storage, and UI. Empty text is not automatically success. Inspect status and errors before deciding what the user sees.
Usage is metadata, not a billing oracle. Capture the fields returned for observability, but calculate product budgets from the official model pricing and account contract applicable at the time. Do not hard-code a price into the response parser.
Choose an owner for conversation state
A simple follow-up can connect to a preceding response using previous_response_id:
follow_up = client.responses.create(
model=os.environ.get("OPENAI_MODEL", "gpt-5-mini"),
previous_response_id=response.id,
input="Now give one counterexample.",
)
This reduces application code that resends context, but the database still needs to associate the product conversation with response identifiers. If the product requires direct retention control, replay, redaction, or provider portability, the application can store a curated history and submit it explicitly instead. Do not ambiguously combine both strategies; a retry can duplicate context or attach the wrong user’s chain.
Store user identity, product-conversation identity, and OpenAI response id separately. Authorize access before following an id. Decide whether “regenerate” creates a branch or replaces the visible answer. An audit record should not disappear merely because the interface now shows another candidate.
Classify errors and retries
Connection failure, rate limiting, server failure, invalid input, and authentication failure need different policies. Apply bounded exponential backoff only to transient categories. Invalid parameters and missing permission should not loop. Set a request timeout and propagate upstream cancellation. Logs can include a correlation id, configured model, latency, and status, but not an API key or complete private prompt.
Whether a generation request is safe to retry also depends on application effects. If an answer later sends mail or updates an order, separate model generation from real tool execution. The tool boundary needs authorization, idempotency, and perhaps human approval. A successful model response is not permission to perform a side effect.
Migrate behind an adapter
Define a TextGenerator contract and make the Chat Completions and Responses implementations return the same domain result. Compare them on an offline evaluation first, then route a small amount of internal traffic to the new path. Measure task success, refusal handling, format validity, latency, and token usage. Generative output is nondeterministic, so exact wording is not a reasonable acceptance criterion.
Release through configuration with a tested fallback. Segment monitoring by API path and model. Remove the old adapter only after the new path is stable. Avoid an unrelated prompt and domain-code cleanup during the cutover, because it destroys the value of rollback.
The minimal migration is to freeze old behavior, add a narrow Responses adapter, handle typed output correctly, choose one state-ownership model, and establish error, cancellation, and evaluation gates. The Responses API provides a base for richer tools and modalities, but the first release does not need every capability. Make one text path observable and reversible; each later item type or tool can then receive its own review.