HomeJournalThis post

Responses API vs Chat Completions for Agents

Compare item-based response state with an application-managed message ledger, then map tool calls, retries, instructions, and compaction to explicit owners.

JP
JP Casabianca
AI Engineer and Product Designer · full-stack delivery · Bogotá

Responses API vs Chat Completions is a state-ownership decision for engineers building tool-using agents. This guide maps one agent turn through both APIs so you can choose a ledger, retry policy, and migration seam without losing durable work.

Responses API vs Chat Completions starts with custody

The useful question is not which endpoint looks newer. Ask which system carries model context between turns, which record your product can reconstruct, and which identifiers survive a failure. The OpenAI Responses API represents work as typed input and output items. A response can follow another through previous_response_id, or work can be associated with a conversation. Chat Completions accepts a message array selected and sent by the application for that request.

That difference changes bookkeeping, but it does not transfer business ownership. A support action, payment attempt, approval, or durable tool job still needs an application work ID and an effect receipt. Responses API vs Chat Completions should therefore be evaluated beside the durable AI agent execution boundary: provider context helps conversation state continue, while the product ledger decides whether a real-world action may continue.

The bounded synthetic corpus defines one support turn with a user message, an order lookup call, its result, and a final answer. It serializes the same domain events into both API shapes and prints preserved, regenerated, and application-owned identifiers. Those labels describe the local corpus, not observed provider latency, pricing, or production reliability.

Two state-custody riversResponses items flow beside a Chat Completions message ledger, with application-owned state marked by dashed banks.Responses item channelApplication message channelmessagetool callresultmessages[]appendreplay
  1. Solid river: response items may be chained by a response identifier or conversation.
  2. Dashed river: the application sends the Chat Completions ledger it chooses to retain.
  3. Both banks: policy, durable work IDs, and business effects remain application concerns.
Figure 1: Custody differs, but neither API becomes the system of record for product work.

Read Responses as an item stream

A response is more than an assistant string. Its output may include message items, tool calls, and other typed records that your loop must inspect. When the next request references a prior response, the provider can recover eligible model context without the client rebuilding an identical message list. That convenience is strongest when a turn naturally produces several item types and the application wants a direct continuation.

The Responses create reference is the source of truth for request fields and current item behavior. Store returned identifiers only for their documented scope; do not treat them as foreign keys for invoices, jobs, or audit events. Responses API vs Chat Completions becomes safer when response IDs are pointers in an inference log, never the only evidence that a tool effect happened.

Instructions deserve special care. A continuation link is not permission to assume every new instruction is inherited exactly as your product intends. Make the instruction policy explicit in the adapter, version it, and test a changed policy in the next turn. The fixture mutates its instruction version and shows that policy identity remains application-owned even when prior response items are available.

Treat Chat messages as an explicit projection

Chat Completions makes the request ledger visible because the application constructs messages before each call. That can be valuable when a team already owns compaction, redaction, replay, and provider portability. It can also be dangerous when code appends opportunistically: duplicated tool results, missing role boundaries, and stale instructions then become ordinary array bugs rather than obvious state transitions.

Build the message list from a canonical domain turn instead of editing an array in place. Select the system or developer policy, user content, assistant tool request, tool result, and any compacted summary through named projection rules. A Chat Completions migration should keep that projector available until matched fixtures prove the new adapter preserves accepted meaning. Responses API vs Chat Completions is then a comparison between projections, not between two unrelated implementations.

Explicit history also means explicit data governance. Decide which content may be retained, how long it persists, what is redacted, and which source events can regenerate it. The Structured Outputs semantic validation pattern belongs after decoding either API: syntactic conformance does not prove that a tool argument is allowed or still valid.

State ownership shelvesThree shelves assign provider-linked, request-carried, and durable application records.Provider-linked response itemsExplicit application message historyBusiness truth and effect receipts
RecordResponsesChat Completions
Prior model contextChain or resendResend selected messages
Tool resultTyped itemTool message
Durable effectApplication ownsApplication owns
Figure 2: A custody table prevents state convenience from being mistaken for durable ownership.

Map a tool round trip without collapsing roles

A tool round trip contains at least four facts: the model requested a named operation, the application validated it, an executor produced a result or error, and the model received a representation of that outcome. Responses uses typed call and result items. Chat Completions expresses the same relationship through assistant tool calls and tool-role messages. Your normalized turn should retain the call ID across either serialization.

Do not insert a raw executor object into the model ledger. Create a bounded result envelope with status, safe fields, provenance, and an application work ID. The bounded programmatic tool calling guide explains why validation and limits belong before execution. Responses API vs Chat Completions does not change that trust boundary; it changes where the inference-facing copy of the envelope is carried.

The local artifact assigns call call_lookup_1 to the frozen lookup and work work_order_17 to the application effect record. Its normalization proves that both adapters preserve the call relationship while only the application record owns the durable work key. These are bounded fixture identifiers emitted by committed code, not identifiers captured from a live OpenAI request.

This custody fixture changes instructions, retries work, and compacts source identifiers so both API adapters expose their ownership decisions.

Runnable artifact — responses-chat-state-fixture.mjs

import assert from "node:assert/strict";
import crypto from "node:crypto";

const turn = { messageId: "msg_1", callId: "call_lookup_1", resultId: "result_1", workId: "work_order_17" };
const instruction = (version) => ({ owner: "application", version, text: version === 1 ? "Quote inventory" : "Quote inventory and cite warehouse" });
const responsesAdapter = (attempt, version, previousResponseId = null) => ({
  attempt, previousResponseId, instructions: instruction(version),
  items: [{ type: "message", id: turn.messageId }, { type: "function_call", call_id: turn.callId }, { type: "function_call_output", call_id: turn.callId, id: turn.resultId }],
});
const chatAdapter = (attempt, version) => ({
  attempt, instructions: instruction(version),
  messages: [{ role: "user", id: turn.messageId }, { role: "assistant", tool_calls: [{ id: turn.callId }] }, { role: "tool", tool_call_id: turn.callId, id: turn.resultId }],
});
const sourceIds = (kind, payload) => [...new Set(kind === "responses"
  ? payload.items.map((item) => item.id || item.call_id)
  : payload.messages.flatMap((item) => [item.id, item.tool_call_id, item.tool_calls?.[0]?.id]).filter(Boolean))];
const compact = (ids) => ({ owner: "application-generated", sourceIds: [...ids], summaryId: "summary_" + crypto.createHash("sha256").update(ids.join("|")).digest("hex").slice(0, 12) });

const originalResponses = responsesAdapter(1, 1);
const changedResponses = responsesAdapter(1, 2, "resp_fixture_1");
const retryResponses = responsesAdapter(2, 2, "resp_fixture_1");
const changedChat = chatAdapter(1, 2);
const retryChat = chatAdapter(2, 2);
assert.equal(originalResponses.instructions.version, 1);
assert.equal(changedResponses.instructions.version, 2);
assert.equal(changedResponses.instructions.owner, "application");
assert.equal(retryResponses.attempt, 2);
assert.equal(retryChat.attempt, 2);
assert.equal(changedResponses.previousResponseId, "resp_fixture_1");
const responseIds = sourceIds("responses", retryResponses);
const chatIds = sourceIds("chat", retryChat);
assert.deepEqual(responseIds, [turn.messageId, turn.callId, turn.resultId]);
assert.deepEqual(chatIds, responseIds);
const beforeCompaction = compact(responseIds);
const afterCompaction = compact([...responseIds, "msg_2"]);
assert.notEqual(beforeCompaction.summaryId, afterCompaction.summaryId);
assert.equal(beforeCompaction.owner, "application-generated");
const effects = new Map();
for (const attempt of [changedResponses, retryResponses]) effects.set(turn.workId, { workId: turn.workId, resultId: turn.resultId, latestAttempt: attempt.attempt });
assert.equal(effects.size, 1);
assert.deepEqual(effects.get(turn.workId), { workId: "work_order_17", resultId: "result_1", latestAttempt: 2 });
console.log(JSON.stringify({ instructionVersions: [1, 2], retryAttempts: [1, 2], compaction: beforeCompaction, workOwner: "application", providerPointerOwner: "provider-reference" }));
console.log("PASS: two adapters preserve one application work id");

Run node responses-chat-state-fixture.mjs. Expected receipt: PASS: two adapters preserve one application work id.

Design retries around effects, not requests

A network failure can leave three different uncertainties: the provider may not have accepted the inference request, the model may have emitted a tool call you did not receive, or the application may have committed a tool effect before its acknowledgement disappeared. Retrying the whole turn blindly can repeat inference and effects. The coordinator must reconcile each boundary independently.

For provider calls, preserve the request fingerprint and response pointer when documentation supports it. For tools, require idempotency or a lookup by application work ID. For the final answer, decide whether regeneration is acceptable or whether a stored accepted answer must be replayed. Responses API vs Chat Completions offers different context handles, yet neither endpoint can infer whether your warehouse reservation already committed.

The frozen retry case marks the model answer as regenerable, the lookup result as reusable, and the durable work receipt as authoritative. The fixture mutates a transport-attempt number while keeping the work ID stable. That separation is the practical reason to avoid making previous_response_id or the last messages-array index your product's resume cursor.

Compact only through a named boundary

Long-running agents eventually need less context. Compaction is not deletion by token count; it is a semantic projection with an input range, output summary, omissions, and a versioned policy. Keep durable facts and effect receipts outside the compacted prompt. Record which source event range a summary covers so later repair can rebuild or replace it.

With Chat Completions, the application commonly chooses the retained messages and inserts a summary. With Responses, provider-managed context can reduce client resending, but the application still decides when its own domain history is summarized or discarded. Responses API vs Chat Completions should be tested at a compaction boundary because ordinary happy-path turns conceal which assumptions are no longer reconstructible.

The teaching corpus compacts the first message and tool result into a summary item while retaining their source IDs in an application receipt. Its expected output lists the summary ID as regenerated and the source range as application-owned. The background mode vs Batch API comparison is useful when a turn also outlives an interactive request; execution lifetime is a separate axis from context custody.

Migration seam mapA folded seam connects instructions, input items, tools, and output parsing across two API adapters.instructionsResponses adaptermessage policyChat adapterdomain turn
  • The domain turn names user input, requested tools, and accepted results.
  • Each adapter serializes that turn into its API-specific representation.
  • A comparison fixture checks what survives retries, changed instructions, and compaction.
Figure 3: Migrate at an explicit adapter seam instead of translating scattered calls in place.

Place the migration seam around a domain turn

Define one internal turn with policy version, user input, available tools, prior accepted facts, requested call, validated result, and completion status. Write one serializer for Responses and one for Chat Completions. The OpenAI migration guide can then inform field mapping without forcing product code to understand both wire formats everywhere.

Run matched cases for a simple answer, tool success, tool error, changed instructions, retry, compaction, and missing prior state. Compare normalized outcomes rather than raw JSON equality. A Responses API vs Chat Completions decision should be reversible at this seam until operational evidence supports removing the older adapter. Avoid claims about universal speed or cost unless both paths are benchmarked with committed inputs and equivalent service settings.

Log the adapter name, API model, policy version, domain-turn hash, provider response ID when present, and application work IDs. Exclude sensitive prompt content by default. This receipt lets a reviewer determine whether a difference came from state projection, model behavior, tool execution, or product policy.

Choose the ledger you can operate

Choose Responses when typed items, integrated tools, and provider-linked continuation reduce application ceremony while fitting your retention policy. Choose Chat Completions when an explicit application-supplied message projection is a deliberate requirement or an existing system already operates it well. Mixed estates are normal during migration, but a single run should have one authoritative adapter path and one application work ledger.

Before shipping, test expired or unavailable prior state, duplicate tool delivery, instruction replacement, rejected arguments, an effect committed before timeout, and compaction repair. Document what the provider stores, what the client resends, what the product persists, and which record allows recovery. Responses API vs Chat Completions is successful when an on-call engineer can answer those custody questions from the receipt without reconstructing an array by intuition.

Keep the conclusion narrow: the Responses state model is often the more direct fit for new item-oriented agents, while Chat Completions remains workable with disciplined message ownership. The right choice is the one whose context behavior and failure recovery your team can explain, test, and reverse.

Continue into opaque stateless continuity

Preserve encrypted reasoning items for stateless agents without claiming plaintext visibility or product ownership of hidden reasoning.