# Migrate Function Calling from Chat Completions to Responses API

> Move the whole function-calling loop: validate the model’s arguments, run the authorized handler, return the result with its original call_id, and continue until the user gets a final answer. The JavaScript example preserves typed state and handles failed or unfinished responses.

- URL: https://blog.laozhang.ai/en/posts/chat-completions-to-responses-api-function-calling-migration
- Published: 2026-07-28
- Updated: 2026-10-07
- Author: LaoZhang AI Team (https://blog.laozhang.ai/en/about)
- Topic: API Guides
- Tags: OpenAI API, Responses API, Chat Completions, Function Calling, JavaScript

---
To migrate function calling from Chat Completions to the Responses API, change the tool definition and **the entire execution loop**. Read every typed output item, validate each function call, run it in your application, return a `function_call_output` with the original `call_id`, then make another Responses request. Finish only when you have both the required application result and an acceptable final answer.

Custom functions still run in your application. A model-generated call is a request to do work; HTTP 200 or a sentence saying “I checked your order” does not prove that work happened. OpenAI [recommends Responses for new projects while continuing to support Chat Completions](https://developers.openai.com/api/docs/guides/migrate-to-responses). An existing application should preserve its working business behavior during the move.

This guide keeps the JavaScript implementation complete: a scoped shipment lookup, argument validation, an allowlisted handler, matched tool outputs, manual state replay, a streaming adapter, and a recovery plan. The code uses demonstration data. Its application logic was checked offline with fake Responses and synthetic events; no SDK integration, live model, real account, or production shipment system was tested.

## The contract changes at eight points

| Concern | Chat Completions | Responses API | What to preserve |
| --- | --- | --- | --- |
| Request | `client.chat.completions.create()` | `client.responses.create()` | Compatible model and effective configuration |
| Conversation input | `messages` | `input` items | User request, instructions, relevant history |
| Returned data | `choices[0].message` | Heterogeneous `response.output` | Every applicable typed item |
| Function definition | `{ type: "function", function: { … } }` | `{ type: "function", name, parameters, strict, … }` | Tool meaning and schema version |
| Function request | `message.tool_calls` | `type: "function_call"` items | Name, arguments, original `call_id` |
| Application result | `role: "tool"`, `tool_call_id` | `type: "function_call_output"`, `call_id` | The result joined to its actual call |
| State | Resend messages | Manual replay, response chaining, or Conversation | One coherent state strategy |
| Streaming | `choices[].delta` | Typed Responses events | Complete calls before execution |

These shapes come from the current [migration](https://developers.openai.com/api/docs/guides/migrate-to-responses) and [function-calling](https://developers.openai.com/api/docs/guides/function-calling) guides. Do not translate tool results into assistant messages or use an item’s `id` in place of `call_id`. The optional item ID identifies the output item; `call_id` identifies the function call whose result you are returning.

![Illustrative function-call and result cards distinguish the item ID from the shared call_id and show typed history for replay](https://blog.laozhang.ai/posts/en/chat-completions-to-responses-api-function-calling-migration/img/result-state-ids.webp)

The result carries the original function call ID; replay also preserves relevant typed state.

`response.output_text` is an SDK convenience for aggregated text, not the full response state or a universal raw REST field. The output array can contain messages, reasoning, and zero, one, or several function calls. A loop that reads only `output[0]` can silently lose work.

## Freeze a baseline before changing the endpoint

Start with a harmless application read that already works through Chat Completions. For example, ask for shipment `ORD-1042` in a test account. Save the normalized request, instructions, tool schema, validated arguments, handler invocation count, actual result, and final answer. Keep latency and token measurements separate from functional acceptance.

For the candidate Responses path, keep the same effective model and reasoning configuration when compatible. As of October 7, 2026, the [current model compatibility guide](https://developers.openai.com/api/docs/guides/latest-model#gpt-6-astra-update-api-and-model-parameters) says GPT-6 Astra and GPT-6.1 Sol require Responses for tool calling, even though they support non-tool Chat requests. GPT-6 Sol and Luna support Chat function calling only with `reasoning_effort: "none"`. Responses uses `reasoning.effort`; Astra and 6.1 Sol do not support `none`. With non-`none` reasoning, remove unsupported sampling and logprob parameters. Check your selected model’s contract rather than changing models as an incidental part of the migration.

Before moving a schema, choose its strictness deliberately. With explicit `strict: true`, each object needs `additionalProperties: false`, and every property must be in `required`. Optional values can use a nullable type while remaining required. An incompatible explicit strict schema is rejected. If you omit `strict`, Responses attempts strict normalization where possible and otherwise falls back to best-effort calling; omission guarantees neither strict output nor rejection. Chat remains non-strict by default, and explicit `strict: false` requests best effort in Responses. These distinctions are documented in [strict mode](https://developers.openai.com/api/docs/guides/function-calling#strict-mode).

The example below uses a simple explicit strict schema and still validates every argument inside the application. Schema compliance does not authenticate a user, authorize access to an order, or make a returned value true.

## A complete Responses function loop in JavaScript

Save the first block as `lookup-loop.mjs`. It exports the whole application loop without creating an SDK client. That makes the same code testable with a fake transport before connecting it to OpenAI.

The trusted account ID comes from the application session, never from model arguments. The demonstration lookup uses an account-and-order key, so even a syntactically valid order ID cannot read another account’s row. In your application, replace the local map with a query that enforces the same access rule.

```js
export const tools = [{
  type: "function",
  name: "lookup_shipment",
  description: "Read the shipment status for an order in the signed-in account.",
  strict: true,
  parameters: {
    type: "object",
    properties: { order_id: { type: "string" } },
    required: ["order_id"],
    additionalProperties: false
  }
}];

// Demonstration data only; the account comes from your authenticated session.
const shipments = new Map([
  ["account-demo:ORD-1042", { status: "packed", dispatch_date: "2026-10-08" }]
]);
const handlers = Object.freeze({
  async lookup_shipment(args, accountId) {
    const row = shipments.get(`${accountId}:${args.order_id}`);
    return row ? { ok: true, order_id: args.order_id, ...row }
      : { ok: false, error: "not_found_in_account" };
  }
});

function checkCall(call) {
  if (typeof call.call_id !== "string" || !call.call_id.trim() ||
      typeof call.name !== "string" || typeof call.arguments !== "string") {
    throw new Error("Malformed function-call envelope");
  }
  if (call.async === true || call.namespace != null ||
      (call.caller != null && call.caller.type !== "direct") ||
      (call.status != null && call.status !== "completed")) {
    throw new Error("Unsupported function-call mode");
  }
}

function validatedArgs(call) {
  if (!Object.hasOwn(handlers, call.name)) throw new Error("unknown_tool");
  let args;
  try { args = JSON.parse(call.arguments); }
  catch { throw new Error("invalid_json"); }
  if (args === null || Array.isArray(args) || typeof args !== "object" ||
      Object.keys(args).length !== 1 || !Object.hasOwn(args, "order_id") ||
      typeof args.order_id !== "string" || !/^ORD-\d{4}$/.test(args.order_id)) {
    throw new Error("invalid_order_id");
  }
  return args;
}

function executeOnce(call, accountId, ledger) {
  const signature = JSON.stringify([call.name, call.arguments]);
  const existing = ledger.get(call.call_id);
  if (existing) {
    if (existing.signature !== signature) throw new Error("Reused call_id changed");
    return existing.pending;
  }
  // Schedule execution after the promise has been stored: concurrent duplicates
  // share the pending result instead of starting a second handler.
  const pending = Promise.resolve().then(async () => {
    let result;
    let args;
    try { args = validatedArgs(call); }
    catch (error) { result = { ok: false, error: error.message }; }
    if (args) {
      try { result = await handlers[call.name](args, accountId); }
      catch { result = { ok: false, error: "lookup_failed" }; }
    }
    return {
      type: "function_call_output",
      call_id: call.call_id,
      output: JSON.stringify(result)
    };
  });
  ledger.set(call.call_id, { signature, pending });
  return pending;
}

export async function runLookup({ create, toInput, model, accountId,
  question, reasoning, maxRounds = 5 }) {
  if (!model || !accountId || typeof question !== "string" || !question.trim()) {
    throw new Error("model, authenticated accountId, and question are required");
  }
  if (!Number.isInteger(maxRounds) || maxRounds < 1) throw new Error("Bad round limit");
  const history = [{ role: "user", content: question }];
  const ledger = new Map(); // One account and one job; not durable storage.
  const instructions = "Look up shipment status using lookup_shipment. " +
    "Report only the returned data. Explain rejected or missing lookups. " +
    "Do not claim a shipment lookup succeeded without a successful tool result.";
  const receipts = [];
  for (let round = 0; round < maxRounds; round++) {
    const response = await create({
      model, instructions, input: structuredClone(history), tools,
      ...(reasoning ? { reasoning } : {}),
      parallel_tool_calls: false, store: false, max_output_tokens: 1200
    });
    if (response.status !== "completed" || response.error) {
      throw new Error(`Generation stopped: ${response.status}; ` +
        `${response.incomplete_details?.reason ?? response.error?.code ?? "unknown"}`);
    }
    if (!Array.isArray(response.output)) throw new Error("Missing typed output");
    const calls = [];
    const ids = new Set();
    const text = [];
    for (const item of response.output) {
      if (!["message", "reasoning", "function_call"].includes(item.type)) {
        throw new Error(`Unsupported output item: ${item.type}`);
      }
      if (item.type === "function_call") {
        checkCall(item);
        if (ids.has(item.call_id)) throw new Error("Duplicate call_id in one response");
        ids.add(item.call_id);
        calls.push(item);
      }
      if (item.type === "message") {
        if (item.status != null && item.status !== "completed") {
          throw new Error("Unfinished assistant message");
        }
        for (const part of item.content ?? []) {
          if (part.type === "refusal") throw new Error("Model refused the request");
          if (part.type === "output_text" && item.phase !== "commentary") {
            text.push(part.text);
          }
        }
      }
    }
    // Use the SDK's replay conversion, rather than reducing state to text.
    // It must retain encrypted reasoning and assistant message phase.
    history.push(...toInput(response.output));
    if (calls.length === 0) {
      const final = text.join("\n").trim();
      if (!final) throw new Error("No final answer");
      if (receipts.length === 0) throw new Error("Lookup never executed");
      const succeeded = receipts.some(item => JSON.parse(item.output).ok === true);
      return { outcome: succeeded ? "lookup_returned" : "lookup_rejected", final, receipts };
    }
    // Sequential dispatch bounds downstream concurrency at one. Every call
    // gets its own original call_id; array position is never the join key.
    const outputs = [];
    for (const call of calls) outputs.push(await executeOnce(call, accountId, ledger));
    receipts.push(...outputs);
    history.push(...outputs);
  }
  throw new Error("Application round limit reached");
}
```

Save this small connection layer as `run-lookup.mjs` beside it. In a server-side project with the current `openai` package installed, it supplies the SDK call and the replay helper documented by OpenAI. It reads credentials through the SDK’s normal server environment; do not put an API key in browser code.

```js
import OpenAI from "openai";
import { toResponseInputItems } from "openai/lib/responses/ResponseInputItems";
import { runLookup } from "./lookup-loop.mjs";

const model = process.env.OPENAI_MODEL;
if (!model) throw new Error("Set OPENAI_MODEL to your compatible model.");
const client = new OpenAI({ timeout: 15_000, maxRetries: 0 });
const effort = process.env.OPENAI_REASONING_EFFORT;
const result = await runLookup({
  create: request => client.responses.create(request),
  toInput: toResponseInputItems,
  model,
  reasoning: effort ? { effort } : undefined,
  accountId: "account-demo", // Demo only; use your authenticated server session.
  question: "What is the shipment status for order ORD-1042?"
});
console.log(JSON.stringify(result, null, 2));
```

When you are ready for an authorized staging call, set `OPENAI_API_KEY`, choose a function-capable `OPENAI_MODEL`, preserve any compatible reasoning effort, and run `node run-lookup.mjs`. The connection layer is a documentation-aligned example; its SDK import path and live execution were not exercised in the offline checks. Confirm that the installed SDK exposes `toResponseInputItems` before rollout; the [current conversation-state JavaScript example](https://developers.openai.com/api/docs/guides/conversation-state) uses that helper.

A successful demonstration lookup should return a receipt with `ok: true`, `order_id: "ORD-1042"`, and `status: "packed"`, followed by a final answer grounded in those values. That is an expected result from the local fixture, not a captured model response. The returned `outcome` records whether a handler supplied a successful result; it does not automatically prove the final text interpreted the result correctly.

### Why the loop waits, rejects, or continues

The loop validates the complete output before dispatching anything. An unsupported output type, malformed `call_id`, unfinished call, or unsupported async/program/namespace mode stops the baseline implementation. Those are distinct orchestration features; accepting them accidentally would leave the application with work it does not know how to manage.

An unknown tool name, invalid JSON, or invalid order ID becomes a matched error output without running a handler. `not_found_in_account` is also an application result. The next model turn can explain either condition, but the caller sees `lookup_rejected` rather than a successful lookup. A refusal, empty final answer, or final answer with no preceding lookup stops the job.

Every accepted call receives its own `function_call_output`; the loop scans the whole array and dispatches sequentially. The initial request sets `parallel_tool_calls: false`, which permits **zero or one** call per turn. It does not guarantee a call or a successful task. If you later enable parallel calls, the loop can process the resulting batch without using array positions as IDs. Add bounded concurrency only for independent operations. See the documented [parallel-calling and tool-choice behavior](https://developers.openai.com/api/docs/guides/function-calling#parallel-function-calling).

The ledger stores a pending promise before execution can begin. Repeated calls with the same ID and arguments within this job reuse the saved result; a changed signature under the same ID stops execution. Duplicate IDs inside a single response are rejected before dispatch. This is bounded, in-process deduplication. It is useful for the read-only example, but is not durable business idempotency.

Five rounds, one downstream lookup at a time, a 1,200-token output cap, and the connection layer’s 15-second request timeout are demonstration limits. Choose limits for your own service. `max_output_tokens` covers visible output and reasoning; `max_tool_calls` limits built-in tools, not your custom dispatcher’s rounds or downstream operations. The current [Responses create reference](https://developers.openai.com/api/reference/resources/responses/methods/create) defines those request parameters. Track a separate end-to-end deadline and business-operation budget when your application requires them.

## Choose one state strategy

For the first migration, manual replay makes it easy to inspect what the next request receives. The program calls `toResponseInputItems(response.output)` and appends the matched function outputs afterward. It preserves returned reasoning and assistant message metadata instead of rebuilding history from the final text. Current [conversation-state guidance](https://developers.openai.com/api/docs/guides/conversation-state) says encrypted reasoning is returned by default and calls out preserving assistant `phase`; `reasoning.encrypted_content` also remains available as an explicit `include` field.

| Strategy | What the next request sends | Main decision |
| --- | --- | --- |
| Manual replay | Relevant prior input, replayable output items, then tool outputs | Your application owns ordering, trimming, retention, and recovery |
| `previous_response_id` | Prior response ID plus new input, such as the matched function outputs | The ID must remain available; resend top-level instructions |
| Conversation | A persistent Conversation identifier and new input | Inputs and outputs are appended after completion; manage access and long-lived state |

Do not resend a full history and also chain to that same history. Do not combine `previous_response_id` and `conversation` in one request. Top-level instructions are not carried over by response chaining, so supply them again. These rules are in the [state guide](https://developers.openai.com/api/docs/guides/conversation-state) and [create reference](https://developers.openai.com/api/reference/resources/responses/methods/create).

### Does `previous_response_id` work over HTTP?

Yes. OpenAI documents response chaining over HTTP as well as WebSocket mode. A valid HTTP continuation can send the saved response ID and a `function_call_output` whose `call_id` matches a call in that prior response. WebSocket continuation uses the same lineage concept but adds a connection-local cache with its own recovery behavior. See the [HTTP chaining examples and WebSocket distinction](https://developers.openai.com/api/docs/guides/conversation-state#previous_response_id-in-websocket-mode).

If your service returns an error saying continuation is “only supported on Responses WebSocket v2,” first identify the actual endpoint, provider, deployment, and request shape. That wording does not establish an OpenAI-wide HTTP limitation. A gateway may implement only part of Responses. Check whether the ID belongs to that route, whether the original response is available, and whether you supplied the actual call’s ID rather than its item ID. Do not switch transports merely to hide a missing `call_id`.

A `store: false` request should be paired with a usable manual replay path rather than relying on ordinary stored response retrieval. It does not enroll your project in Zero Data Retention or erase application and gateway logs. Standard stored Responses have a default 30-day application-state period, subject to documented exceptions; Conversation items are not subject to that 30-day TTL. Chained previous input still contributes to billed context. Check the [data controls](https://developers.openai.com/api/docs/guides/your-data) and [state retention guidance](https://developers.openai.com/api/docs/guides/conversation-state) for your organization and features before choosing a persistence strategy.

## Migrate the streaming reducer, not just the endpoint

Streaming needs a typed event consumer. For custom function calls, register the call from `response.output_item.added`, accumulate argument deltas by `item_id` and `output_index`, reconcile the completed arguments, and verify the completed call item. Never execute partial JSON.

The exact [`response.function_call_arguments.done` schema](https://developers.openai.com/api/reference/resources/responses/streaming-events#response.function_call_arguments.done) contains `arguments`, `item_id`, `output_index`, `sequence_number`, and `type`. It does **not** contain a complete function item or a `call_id`. Keep the original ID from the actual call item; [`response.output_item.done`](https://developers.openai.com/api/reference/resources/responses/streaming-events#response.output_item.done) supplies that completed item. Do not synthesize a call ID from the argument event.

![Illustrative streaming timeline buffers function arguments and waits for a completed response before application execution](https://blog.laozhang.ai/posts/en/chat-completions-to-responses-api-function-calling-migration/img/stream-execution-gate.webp)

The timeline shows this example’s release policy: a failed or unfinished stream does not reach the dispatcher.

Save the following as `completed-stream.mjs`. Its conservative release policy waits for `response.completed` before returning anything to the dispatcher. Completing one argument item does not prove the whole response completed. It also verifies repeated event identities and rejects missing or conflicting call data.

```js
export async function readCompletedStream(events) {
  const pending = new Map();
  const seen = new Map();
  for await (const event of events) {
    if (Number.isInteger(event.sequence_number)) {
      const fingerprint = JSON.stringify(event);
      if (seen.has(event.sequence_number)) {
        if (seen.get(event.sequence_number) !== fingerprint) {
          throw new Error("Conflicting repeated stream event");
        }
        continue;
      }
      seen.set(event.sequence_number, fingerprint);
    }
    if (event.type === "error" || event.type === "response.failed" ||
        event.type === "response.incomplete") {
      throw new Error(`Stream stopped: ${event.type}`);
    }
    if (event.type === "response.output_item.added" &&
        event.item.type === "function_call") {
      const item = event.item;
      if (!item.id || !item.call_id || pending.has(item.id)) {
        throw new Error("Invalid stream call registration");
      }
      pending.set(item.id, { item, index: event.output_index, delta: "", args: null, done: null });
    }
    if (event.type === "response.function_call_arguments.delta" ||
        event.type === "response.function_call_arguments.done") {
      const state = pending.get(event.item_id);
      if (!state || state.index !== event.output_index || state.done) {
        throw new Error("Arguments without a matching open call item");
      }
      if (event.type.endsWith(".delta")) {
        if (state.args !== null) throw new Error("Delta after completed arguments");
        state.delta += event.delta;
      } else {
        if (state.args !== null || (state.delta && state.delta !== event.arguments)) {
          throw new Error("Conflicting completed arguments");
        }
        state.args = event.arguments; // The event does NOT supply a call_id.
      }
    }
    if (event.type === "response.output_item.done" &&
        event.item.type === "function_call") {
      const state = pending.get(event.item.id);
      if (!state || state.index !== event.output_index || state.done ||
          state.args === null || state.args !== event.item.arguments ||
          state.item.call_id !== event.item.call_id || state.item.name !== event.item.name) {
        throw new Error("Completed call does not match buffered arguments");
      }
      state.done = event.item;
    }
    if (event.type === "response.completed") {
      const response = event.response;
      if (response.status !== "completed" || !Array.isArray(response.output)) {
        throw new Error("Invalid completed response");
      }
      const calls = response.output.filter(item => item.type === "function_call");
      if (calls.length !== pending.size) throw new Error("Missing final stream call");
      for (const call of calls) {
        const done = pending.get(call.id)?.done;
        if (!done || done.call_id !== call.call_id || done.name !== call.name ||
            done.arguments !== call.arguments) throw new Error("Unmatched final call");
      }
      return response; // Only now can runLookup validate and execute anything.
    }
  }
  throw new Error("Stream ended before response.completed");
}
```

To use this adapter, add its import to `run-lookup.mjs` and replace the `create` callback:

```js
import { readCompletedStream } from "./completed-stream.mjs";

// Inside the runLookup options:
create: async request => readCompletedStream(
  await client.responses.create({ ...request, stream: true })
),
```

That replacement is a connection snippet for the preceding complete program, not a separate executable file. Text and reasoning events remain in the final response used for replay; the reducer only buffers the call details it needs to check. It consumes decoded SDK events rather than raw network bytes. If you implement raw SSE parsing yourself, separately test incremental UTF-8 decoding, split frames, and cancellation.

`response.failed` and `response.incomplete` stop this adapter. The non-streaming loop also rejects every status other than `completed`, including `queued`, `in_progress`, and `cancelled`. On an incomplete response, inspect `incomplete_details` before choosing a next step: a token limit, content filter, or other documented reason calls for different handling. The relevant [streaming guide](https://developers.openai.com/api/docs/guides/streaming-responses) and [incomplete event reference](https://developers.openai.com/api/reference/resources/responses/streaming-events#response.incomplete) distinguish these states. Transport EOF without `response.completed` is a failed collection attempt, even if the UI already displayed text.

## Recover without repeating a business operation

For a shipment read, retrying the lookup can be acceptable after you classify the failure. A refund, email, booking, or payment needs a different recovery contract. The model’s `call_id` is a protocol join, not permission to act and not an operation key that survives a new model-generated call.

Before a write, derive a business operation key from trusted application data—for example, an authorized refund request ID scoped to the account. Reserve it atomically in durable storage, record the downstream operation identifier, commit the result, and return that saved result on retry. A second worker must see the reservation or committed result before it can act. If the downstream service supports idempotency keys, use the same business key there too. This is application design, not an API guarantee of exactly-once execution.

A timeout after submitting a write can leave the outcome unknown. Query the downstream operation or reconcile your ledger; do not label it “failed” and repeat it blindly. If the function succeeded but the next model request failed, keep the committed tool result and resume the model step with the corresponding state. Do not restart the business action to regenerate the final sentence.

Separate request retries from operation retries. The example disables automatic SDK retries in its connection layer so failures are visible during staging, but that alone cannot prevent duplicate writes elsewhere. A production retry policy needs the durable ledger, suitable downstream reconciliation, and a bounded request budget.

Keep a recoverable execution record with the job ID, account scope, response ID, call ID, tool/schema version, sanitized arguments or safe references, operation key, committed result reference, and failure class. Protect this record as application data. General logs should omit keys, sensitive payloads, and unnecessary customer content.

## Diagnose failures by the broken contract

| Symptom | First check | Next action |
| --- | --- | --- |
| Schema rejected | Flat function definition, explicit strict constraints | Correct every object’s `additionalProperties` and required properties |
| No function call | Submitted tool names, tool choice, complete output types | Confirm the model may call the tool; do not execute merely because text mentions it |
| `function_call_output requires call_id` | Actual `function_call.call_id` and outgoing result | Copy the original call ID unchanged; never use the item’s `id` |
| “No matching tool call” | State strategy, route, response availability, call/result pairing | Restore the relevant call context and matched output |
| Lookup ran but no final answer | Follow-up request and response status | Continue from the saved result; classify generation failure separately |
| One call disappears | Full output scan and result count | Process every call in the batch, each with its own ID |
| Stream closes after arguments | Item completion versus whole-response completion | Stop before dispatch; diagnose incomplete/error/EOF |
| Instructions or reasoning disappear | Actual next-request input and replay conversion | Preserve phase and reasoning; resend top-level instructions |
| Write happens twice | Durable operation key and downstream receipt | Reconcile and return the committed result before any new attempt |

Empty output, model refusal, rejected arguments, failed generation, and an unknown downstream outcome are different failures. A single “request succeeded” metric cannot tell you whether the user’s task succeeded.

## Staging acceptance checklist

The offline checks for this guide exercised the actual application module with fake transports: a call after a reasoning item, encrypted reasoning and message-phase replay, matched successful results, invalid JSON and argument shapes, a non-allowlisted tool, account isolation, multiple calls, repeated calls, conflicting IDs, refusal, empty output, failed/incomplete status, and the round limit. Synthetic streams exercised split arguments, repeated events, exact `.done` fields without `call_id`, mismatched completed calls, error events, and premature EOF. These checks prove those local branches, not SDK availability or OpenAI server acceptance.

For your rollout, require evidence beyond those fixtures:

- [ ] The old and new paths receive the same safe request, trusted account, instructions, tool/schema version, and compatible model configuration.
- [ ] The authorized handler runs for valid input; malformed, unknown, or out-of-scope requests cannot access or change unrelated data.
- [ ] Every tool output retains its original call ID, and a multi-call batch loses no result.
- [ ] Replayed state preserves relevant reasoning and assistant phase, and instructions survive a second turn.
- [ ] Streaming waits for complete calls and handles incomplete responses, cancellation, duplicate events, and broken connections.
- [ ] The final answer agrees with the actual tool result; no-call text or a model’s promise is not accepted as proof of execution.
- [ ] Write retries, concurrent workers, and process restarts return the same committed business result without repeating the operation.
- [ ] Request duration, application rounds, downstream concurrency, token usage, and operational costs stay within your selected limits.
- [ ] Recovery resumes the correct state and saved result after a model failure or lost connection.
- [ ] A rollback preserves already committed operations and returns traffic to a previously validated model, endpoint, configuration, and state path.

Stop traffic expansion on an unmatched call ID, duplicate write, lost call, policy/state regression, or materially incorrect final answer. Returning to Chat is only a rollback when the selected model and configuration actually support the old tool path. A Responses-only tool model cannot be rolled back by changing one method name.

## Source and provider boundaries

This is an OpenAI custom-function migration. Azure, AliCloud, and compatible gateways may differ in base paths, authentication, deployment IDs, response-ID lifetime, retention, and event support. A familiar SDK shape or HTTP 200 is not proof of feature parity. Validate the complete loop on the route you intend to serve; do not transplant a provider-specific restriction into OpenAI’s contract.

If authentication is the unfinished step, use the [API key setup guide](https://blog.laozhang.ai/en/posts/openai-api-key-free-trial) to establish the first server-side request. If you need a typed final object instead of an application action, the [Structured Outputs versus function calling guide](https://blog.laozhang.ai/en/posts/structured-outputs-vs-function-calling) explains that choice and when both belong in one flow.

## FAQ

### Do I have to migrate every Chat Completions application now?

No. Chat Completions remains supported, while OpenAI recommends Responses for new projects. Migrate when the chosen model or required features need it, or when the state and tool interfaces fit your application better. Preserve a validated old path where compatible. See the [migration guide](https://developers.openai.com/api/docs/guides/migrate-to-responses).

### Does `strict: true` make the function run safely?

It makes compatible arguments adhere to the declared schema. Your application still owns validation, access control, execution, idempotency, and checking the actual result. A valid order ID does not authorize access to that order. The [strict-mode contract](https://developers.openai.com/api/docs/guides/function-calling#strict-mode) governs shape, not business permission.

### Can I use `previous_response_id` for HTTP function results?

Yes, OpenAI supports HTTP chaining. Send the available prior response ID and the result with the original call’s `call_id`, and resend your instructions. A gateway may have a different implementation; inspect the real route before treating its error as a universal API rule. The [conversation-state guide](https://developers.openai.com/api/docs/guides/conversation-state) documents both HTTP and WebSocket continuation.

### Is `response.function_call_arguments.done` enough to execute?

It tells you the argument string is finalized. You still need the actual call item’s name and original call ID, application validation and authorization, and your chosen response-completion policy. This guide waits for `response.completed`. The [argument-done schema](https://developers.openai.com/api/reference/resources/responses/streaming-events#response.function_call_arguments.done) does not supply `call_id`.

### Can I use `call_id` as the idempotency key for a refund?

Use it to match the tool result, and use a separate durable business key to deduplicate the refund. A new call can propose the same operation under a new protocol ID. After a timeout, reconcile the downstream outcome before retrying. The in-memory ledger in this guide demonstrates bounded lookup deduplication, not restart-safe refunds.
