DEV Community

xn
xn

Posted on • Originally published at xbstack.com

OpenAI Responses API Stream Abort: Fix No tool call found

A production-focused update based on a real project: A function_call can be visible before it is durable in Conversation state. This guide explains the 400 No tool call found erro…

OpenAI Responses API: Why Stream Abort Causes No tool call found for function call output

Here is the production conclusion first: seeing a streamed function_call is not the same as having a durable call that the Conversation can reference later. If a client closes the Responses API stream before the response commits, but the application already executes the tool, the next turn may reject the matching function_call_output with 400 No tool call found for function call output. The error is bad; the orphaned side effect is worse. A payment, deployment, email, or database write may already exist even though the model conversation has no call record for it.

The fastest recovery is not to keep retrying the stale output. Read the Conversation Items first. If the matching call_id exists, reuse or produce the tool result idempotently and submit it. If the call is absent, treat the streamed call as uncommitted, discard that call_id, and generate a new turn. Separately, every external write needs an application-owned idempotency key, because durable model state does not provide business exactly-once execution.

The server-side behavior described here comes from OpenAI Python SDK Issue #3561. I did not use an unapproved production API key and do not present the local lab as an independent server reproduction. The lab makes the reported persistence boundary explicit and tests which recovery policies create or prevent orphaned side effects.

1. What the 400 Error Actually Means

The representative error is:

400 No tool call found for function call output with call_id call_xxx.
Enter fullscreen mode Exit fullscreen mode

This is not a missing Python function and not a JSON Schema validation failure. It is an association failure. A function_call_output says, in effect, “this value belongs to the tool call identified by call_id.” The server must be able to find that call in the current state before it can continue the model turn. If no matching call exists, accepting the output would attach data to an event that the conversation does not know happened.

Start by separating three failure classes. The first is wiring: the output was sent to the wrong Conversation, a stale ID was reused, or concurrent tabs crossed user state. The second is state-mechanism mismatch: one turn used a Conversation while another turn was built against a different context path. The third is the abort window covered here: the client observed a call in the stream, but the response never reached the state where that call was durably available in Conversation Items.

Issue #3561 documents that exact path. The reporter creates a Conversation, starts a streamed Responses request, receives a function_call in response.output_item.added, closes the stream, and then lists Conversation Items. The list is empty. The local tool is nevertheless executed. Submitting its output to the same Conversation on the next turn fails with the 400 error. The report uses Windows 10, Python 3.11.5, and openai 2.45.0.

2. Official Status as of August 5, 2026

Version numbers can be misleading here. The latest OpenAI Python SDK release is v2.53.0, published on August 3, 2026. Issue #3561 remains open, carries the bug label, and was updated on August 4. It has no linked Python fix pull request, milestone, or release note identifying a first fixed version.

The accurate statement is therefore limited: there is a runnable report in the official repository, and the report has not been closed with a Python release conclusion. You can test 2.53.0 in your own environment, but you should not remove the reconciliation gate merely because the package number is newer than 2.45.0.

Do not automatically downgrade either. The behavior crosses the client stream, the Responses service commit boundary, and Conversation persistence. A downgrade may not change server behavior and can reintroduce unrelated type, authentication, retry, or transport problems. A safer upgrade policy is to freeze the current production version, add a deterministic abort matrix, run the same matrix against a candidate release in an isolated OpenAI Project, and promote only when official status, live integration results, and side-effect counts agree.

The issue links to a related fix in the JavaScript Agents SDK, PR #1241, which reconciles streamed function calls when server-managed runs abort. That relationship makes the failure mode credible across adjacent stacks, but it is not proof that the Python Responses path is fixed. SDK runner behavior, API service behavior, and conversation storage must be verified independently.

3. Stream Visibility Is Not a Commit Token

Streaming exists to expose useful information before a full response completes. response.output_item.added tells the client that an output item has appeared in the current stream. response.function_call_arguments.done tells it that the function arguments are complete. Those events are useful for UI, validation, logging, and scheduling low-risk work. They are not, by themselves, a durable authorization token for an external side effect.

There are at least three distinct guarantees:

Boundary What you know What you still do not know
Client observed an event A call and call_id appeared in the stream The Conversation durably contains the call
Response completed The model turn reached a normal completion boundary The tool will be delivered only once
Conversation reconciliation The server lists the matching function_call The business action is exactly once

The unsafe implementation jumps from the first row directly to execution. That may be tolerable for a cacheable weather read. It is not acceptable for money movement, order creation, deployment, outbound email, access changes, or database mutation. If the stream aborts before commitment, the application has created an external fact without a model-state fact that can receive the result.

call_id is also not a sufficient business idempotency key. It is primarily the correlation ID between a model call and a tool output. A regenerated model turn can produce a new call_id for the same order or deployment. A stable key should come from the business intent, such as tenant + operation + resource + desired_version, with call_id stored as supporting evidence rather than the only uniqueness boundary.

tenant-a:deploy:release-2026-08-05:call_xxx
Enter fullscreen mode Exit fullscreen mode

4. The Local Lab and Its Evidence Boundary

The public lab is located at:

experiments/openai-responses-stream-abort-tool-call-loss/
Enter fullscreen mode Exit fullscreen mode

It uses only the Python standard library. provisional_items represents calls already exposed through stream events but not committed. committed_items represents calls that can be referenced by later outputs. Normal completion moves provisional calls into committed state. Abort discards them in the modeled failure path. Submitting an output for a missing call raises the same semantic error as the official report.

This is not a reverse-engineered copy of OpenAI internals. It does not prove how every production request is stored. Its purpose is narrower and honest: given the officially reported condition that the client can observe a call that the Conversation later lacks, which application policies are safe?

The lab fixes four scenarios:

  1. complete the stream, confirm the call, then execute;
  2. execute immediately after output_item.added, then abort;
  3. abort, reconcile, and execute only if the call exists;
  4. commit the call, deliver the tool job twice, and deduplicate with a business ledger.

It does not measure live API latency, token cost, browser behavior, HTTP/2 teardown, proxy buffering, model tool-selection quality, or the previous_response_id path. OpenAI behavior claims are sourced from official documentation and Issue #3561. Local results support the application control model only.

5. Four Scenarios, One Critical Invariant

The experiment writes the verified result to:

experiments/openai-responses-stream-abort-tool-call-loss/results/verification.json
Enter fullscreen mode Exit fullscreen mode

Four standard-library unit tests pass. The result matrix is:

Scenario Conversation items Deliveries External effects Next turn
Complete, then execute call + output 1 1 accepted
Execute on observed call, then abort none 1 1 rejected with 400
Abort, reconcile, then decide none 0 0 stale call discarded
Committed call delivered twice with idempotency call + output 2 1 accepted

The second row is the failure that matters. The tool side effect count is already 1, while the Conversation contains no items. The output cannot be attached, so the next turn receives:

No tool call found for function call output with call_id call_aborted.
Enter fullscreen mode Exit fullscreen mode

For a read-only tool, generating another turn may be enough. For a charge or deployment, regeneration may produce a second valid call and a second side effect. Refusing every retry avoids duplication but can leave the user with a failed task after the action actually succeeded. That conflict cannot be solved with prompting; it requires an application truth store and idempotent execution.

The third scenario proves the conservative gate: after abort, reconcile first. If the call is missing, do not execute. The fourth scenario proves that reconciliation is not an exactly-once mechanism. A valid committed call can still be delivered repeatedly by a queue, manual retry, or worker recovery.

6. Fast Triage for Production Incidents

Do not begin by rewriting the prompt or randomly changing SDK versions. Follow the association chain.

Step 1: Confirm the Conversation and call_id belong to the same turn

Capture complete correlation fields:

request_id
conversation_id
response_id
call_id
user_id
tenant_id
stream_status
tool_name
business_idempotency_key
Enter fullscreen mode Exit fullscreen mode

Verify that the output is being sent to the same Conversation that produced the call. Check concurrent tabs, retries, and tenant boundaries. Partial IDs are often insufficient when two live requests overlap.

Step 2: Read Conversation Items

List Conversation Items and look for the exact call_id. If it is absent, stop submitting the output. Mark the application intent as uncommitted or orphaned-intent and route it to regeneration or manual reconciliation.

Step 3: Identify how the stream ended

Distinguish normal completion, user stop, component unmount, AbortController, reverse-proxy timeout, mobile network loss, worker restart, and server crash. These paths can look identical to the UI while producing different backend states.

Step 4: Check whether the tool already produced a side effect

Verify the external side effect independently. An empty Conversation does not mean the tool never ran. Check payment records, deployment IDs, message IDs, database constraints, or the application ledger.

Step 5: Choose the recovery path

Use the reconciliation result and the external side-effect state together:

Call in Conversation Side effect Recovery
yes no reserve idempotency key, execute, submit output
yes yes reuse stored result, submit output
no no discard stale call_id and generate a new turn
no yes do not execute again; compensate or reconcile manually, then rebuild model context

The last row should not be hidden behind a generic retry loop. The business system has a fact that the model system lacks. Whether you send that fact back as a normal message, create an auditable recovery event, or require human review depends on the risk and regulatory context.

Safe OpenAI Responses API recovery after a stream abort: reconcile Conversation Items, discard a missing call_id, or reserve a business idempotency key before execution

7. A Production Tool-Call State Machine

A reliable orchestrator needs more than pending/success/failed:

stream_observed
→ call_committed
→ execution_reserved
→ execution_succeeded
→ output_submitted
→ model_acknowledged
Enter fullscreen mode Exit fullscreen mode

Failure branches need names as well:

stream_aborted_before_commit
commit_unknown
execution_unknown
output_rejected
manual_reconciliation_required
Enter fullscreen mode Exit fullscreen mode

stream_observed is safe for UI. It is not safe for a high-risk executor. call_committed means the Conversation reconciliation succeeded. execution_reserved means a unique business key has locked the action. execution_succeeded stores the immutable external result. output_submitted records delivery to the model, and model_acknowledged closes the complete workflow.

What the state record should store

Persist this state in a transactional store, not only in traces. A minimal record should include the business key, tenant and user, Conversation and Response IDs, call_id, tool name, argument digest, every transition timestamp, abort reason, external result ID, result reference, retry count, and last error.

intent_id
business_idempotency_key
user_id
tenant_id
conversation_id
response_id
call_id
tool_name
arguments_hash
stream_observed_at
call_committed_at
execution_reserved_at
execution_succeeded_at
output_submitted_at
model_acknowledged_at
status
abort_reason
external_result_id
result_payload_ref
retry_count
last_error_code
created_at
updated_at
Enter fullscreen mode Exit fullscreen mode

Use a unique constraint on the business idempotency key and another on tenant + conversation_id + call_id.

Production tool-call state machine from stream_observed to model_acknowledged, with abort, unknown execution, and manual reconciliation branches

The argument digest matters. Reusing one business key with different tool arguments should fail closed rather than silently return an old result. The external result ID should be a payment transaction, deployment ID, email Message ID, or other verifiable fact that a recovery worker can query after a crash.

Useful metrics include stream_observed_without_commit_total, tool_execution_before_commit_total, function_call_output_not_found_total, reconciliation_latency_ms, duplicate_delivery_total, idempotency_reuse_total, and manual_reconciliation_total. For high-risk tools, tool_execution_before_commit_total should remain zero. A falling 400 rate is not enough if errors are merely swallowed while side effects diverge.

8. A Safer Python Control Skeleton

The following is a control-flow example, not a complete OpenAI wrapper. The important separation is observed call, committed call, idempotency reservation, tool execution, and output submission:

from dataclasses import dataclass

@dataclass
class ToolIntent:
    conversation_id: str
    response_id: str
    call_id: str
    tool_name: str
    arguments: dict
    business_key: str


def handle_streamed_call(client, intent: ToolIntent, ledger, executor):
    items = client.conversations.items.list(
        conversation_id=intent.conversation_id,
    )

    committed = any(
        item.type == "function_call" and item.call_id == intent.call_id
        for item in items.data
    )
    if not committed:
        return {
            "status": "discarded_uncommitted_call",
            "call_id": intent.call_id,
        }

    existing = ledger.get(intent.business_key)
    if existing:
        tool_result = existing.result
    else:
        reservation = ledger.reserve(intent.business_key, intent.call_id)
        if not reservation.acquired:
            return {"status": "execution_in_progress"}

        tool_result = executor.execute(
            intent.tool_name,
            intent.arguments,
        )
        ledger.complete(intent.business_key, tool_result)

    return client.responses.create(
        conversation=intent.conversation_id,
        input=[{
            "type": "function_call_output",
            "call_id": intent.call_id,
            "output": tool_result,
        }],
    )
Enter fullscreen mode Exit fullscreen mode

Two crash windows remain. The process may fail after checking Conversation Items but before output submission. It may also fail after the external action succeeds but before the ledger is completed. The ledger therefore needs durable reservation, unique constraints, external result lookup, and result reuse. A recovery worker must query the ledger and external system before deciding to execute again.

High-risk tools should add authorization and human approval before execution_reserved. See AI Agent tool authorization for a per-call Policy Gate. If your application uses OpenAI Agents SDK approval interruptions, the separate RunState resume and v0.19.3 persistence guide covers that layer.

9. Recovering Browser Disconnects and Proxy Timeouts

Production disconnects are rarely a clean stream.close(). A browser can background the tab, mobile connectivity can switch networks, a reverse proxy can close the downstream while the upstream is still working, and a server process can die after an event was received but before the log was flushed. The correct state is often “unknown,” not “failed.”

Persist one application record per streamed response:

response_id
conversation_id
last_sequence_number
last_event_type
observed_call_ids
completed_at
abort_reason
reconciliation_status
Enter fullscreen mode Exit fullscreen mode

After an ambiguous disconnect, a background reconciler should list Conversation Items and classify each observed call as committed or missing. Committed calls can enter the idempotent execution queue. Missing calls must be discarded and must not receive an output. If a frontend or another service already executed the tool, the business ledger must route the operation into compensation or manual reconciliation.

Do not give the browser direct ownership of high-risk execution. The browser should receive the stream and present approval. A backend orchestrator should persist the intent, reconcile Conversation state, execute the tool, and submit the output. Closing a page then becomes a presentation failure, not a payment or deployment consistency failure.

10. Regression Tests That Are Worth Keeping

A manual Ctrl-C test is not sufficient. The regression suite should actively abort at several boundaries and verify Conversation state, ledger state, and external effects together.

OpenAI Responses API stream-abort regression matrix comparing abort points, Conversation persistence, side effects, stale output handling, and upgrade decisions

Run commit-boundary tests after response.output_item.added, after response.function_call_arguments.done, after the final argument delta, immediately before completion, and after response.completed. Store the Response ID, call ID, final sequence number, and the Conversation Items result. Do not encode assumptions about which event commits; let the current API behavior populate the matrix.

Run side-effect-gate tests with a transactional fake payment, deployment table, or outbox. Before reconciliation, the count must be zero. After a committed call and first business-key reservation, it must be one. Deliver the same work two, five, or twenty times and assert that the external fact still appears once.

Inject process crashes in at least five windows:

before ledger reservation
after reservation, before tool execution
after tool success, before ledger completion
after ledger completion, before function_call_output
after output submission, before final model response
Enter fullscreen mode Exit fullscreen mode

Each window has a different recovery rule. Retry is safe before reservation. A stale reservation needs ownership and expiry logic. Success before ledger completion requires external lookup. A completed ledger result should be reused. A lost final model response should not execute the tool again.

Keep a version matrix in CI:

SDK Abort point Call persisted Side effects Old output accepted Decision
production after item added measured 0 measured safe/unsafe
production after arguments done measured 0 measured safe/unsafe
production after response completed measured 1 measured baseline
candidate same three points measured invariant measured promote/block

Unit tests should validate the application state machine on every commit. A live integration test should run when the SDK, streaming wrapper, proxy configuration, or cancellation logic changes. Use an isolated OpenAI Project, a low-cost model, a fake tool, and a strict spend limit. Save sanitized Conversation Items, package version, HTTP status, and timestamps as build artifacts.

The public XBSTACK lab deliberately stops before a live API call. A project adopting this pattern should add an environment-gated live_integration_test.py only after explicit credential authorization. That preserves the evidence boundary while making future official fixes comparable under the same test.

11. Common Mistakes and Final Decision

Mistake: treating function_call_arguments.done as a commit. It means argument streaming is complete, not that the Conversation can reference the call.

Mistake: using call_id as the only exactly-once key. A regenerated turn can produce a new call ID for the same order or deployment.

Mistake: retrying the same output after every 400. If the call is absent, retries do not create it.

Mistake: assuming an empty Conversation means the tool did not execute. External systems and the application ledger are the source of truth for side effects.

Mistake: deleting safeguards because the current SDK is newer than the reported version. As of August 5, 2026, the issue remains open and no Python fixed release is named.

The decision is simple:

A streamed tool call is observable data, not an execution credential. Confirm the server committed it, reserve a stable business idempotency key, execute through a backend orchestrator, and reconcile every ambiguous disconnect. If the call ID is missing, discard it.

Read-only tools may accept a looser policy if duplicate cost is low. Payments, email, deployments, writes, and permission changes should not. They need the state machine, ledger, constraints, authorization, and recovery path described above. For broader timeout and retry design, see AI Agent error recovery. For the full tool-call production boundary, continue with AI Agent Tool Use.

Frequently Asked Questions

Why can the client have a call_id while Conversation Items are empty?

The stream event and durable state are different guarantees. The client can observe a provisional function call before the response reaches its persistence boundary. If the response is aborted first, the reported failure path leaves no Conversation item to reference later.

Is waiting for response.completed enough?

It is safer than executing on the added event. For a high-risk tool, still reconcile Conversation Items before execution. Response completion addresses the model-turn boundary; an idempotency ledger addresses duplicate tool delivery.

What if the tool executed but the call_id is missing?

Do not invent a tool call or continue submitting output to the missing ID. Record the real external result, prevent duplicate execution, and start a new auditable model turn or require human reconciliation. The appropriate recovery depends on whether the business allows the model to continue automatically.

Is this the same as the OpenAI Agents SDK RunState persistence issue?

No. This page covers a Responses API Conversation that did not commit a streamed function_call. The RunState article covers approval resume and Session persistence in the Agents SDK. Both can create a mismatch between model state and external action, but the APIs and recovery boundaries differ.

Does the local lab prove the OpenAI server implementation?

No. It tests application policies under the condition documented by the official issue. OpenAI server behavior, affected versions, and the first fixed release must come from official Issues, pull requests, and release notes. This page will update its version conclusion only when new official status and comparable verification are available.


Experiment: OpenAI Responses API Stream Abort Tool Call Loss Lab

Official sources: OpenAI Python SDK Issue #3561 · Function Calling Guide · Conversation State Guide


Canonical article on XBSTACK:https://www.xbstack.com/en/ai/openai-responses-api-stream-abort-tool-call-lost/?utm_source=devto&utm_medium=referral&utm_campaign=openai-responses-stream-abort&utm_content=en%2Fopenai-responses-api-stream-abort-tool-call-lost&ref=devto

标签:#AI #SoftwareEngineering #DeveloperTools #OpenAI Responses API #Function Calling

Top comments (0)