[GH-ISSUE #740] Responses API: No tool output found for tool call <id> when any item sits between function_call and its function_call_output #207

Open
opened 2026-10-26 09:55:06 +00:00 by bsakaguchi · 2 comments
Owner

Originally created by @LiTaoHH on GitHub (Sep 20, 2026).
Original GitHub issue: https://github.com/deepseek-ai/awesome-deepseek-integration/issues/740

Summary

POST /v1/responses rejects a request whose input array contains a function_call and its matching function_call_output that are not strictly adjacent. Any item in between — an extra message (role developer/user) or a reasoning item — makes the pairing check fail with:

HTTP 400
{"error":{"message":"No tool output found for tool call call_...","type":"invalid_request_error","param":"","code":"invalid_request_error"}}

even though the matching function_call_output is present later in the same input array. The OpenAI Responses API pairs tool calls with their outputs by call_id regardless of position, so clients written against that behaviour break here — Codex in particular.

Evidence

Four minimal non-streaming requests to https://api.deepseek.com/v1/responses, model: deepseek-flash, max_output_tokens: 16. They differ only in the position of one extra item.

A — extra message (role developer) between call and output → 400

{
  "model": "deepseek-flash",
  "store": false,
  "stream": false,
  "max_output_tokens": 16,
  "input": [
    {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "say ok"}]},
    {"type": "reasoning", "summary": [], "content": [{"type": "reasoning_text", "text": "thinking"}]},
    {"type": "function_call", "call_id": "call_A", "name": "exec_command", "arguments": "{\"cmd\":\"echo hi\"}"},
    {"type": "message", "role": "developer", "content": [{"type": "input_text", "text": "Team Harness suggested checks: run the project's test suite"}]},
    {"type": "function_call_output", "call_id": "call_A", "output": "hi"}
  ]
}
400 {"error":{"message":"No tool output found for tool call call_A.","type":"invalid_request_error","param":"","code":"invalid_request_error"}}

A2 — extra reasoning item between call and output → 400, same error

Same array as A, with the 4th item replaced by:

{"type": "reasoning", "summary": [], "content": [{"type": "reasoning_text", "text": "thinking"}]}
400 {"error":{"message":"No tool output found for tool call call_A2.","type":"invalid_request_error","param":"","code":"invalid_request_error"}}

B — control: function_call immediately followed by its output → 200

Same array as A with the interleaved item removed. Returns a normal response object.

C — control: the extra item placed after the output → 200

  {"type": "function_call", "call_id": "call_C", "name": "exec_command", "arguments": "{\"cmd\":\"echo hi\"}"},
  {"type": "function_call_output", "call_id": "call_C", "output": "hi"},
  {"type": "message", "role": "developer", "content": [{"type": "input_text", "text": "Team Harness suggested checks: run the project's test suite"}]}

The failure is therefore purely positional: the tool output is present and correctly addressed by call_id, it is only not adjacent.

Note: a request that omits the reasoning item entirely is rejected with The reasoning_text in the thinking mode must be passed back to the API. — that appears to be intentional (thinking mode), and the requests above all include the reasoning item, so it is unrelated to this bug.

Impact on the official Codex integration

This is not a corner case for Codex (https://api-docs.deepseek.com/quick_start/agent_integrations/codex):

  1. Codex appends non-tool items inside a turn — hookSpecificOutput.additionalContext returned by PostToolUse hooks, agent-harness advisories, and reasoning items land between a tool call and its result.
  2. When that happens, the turn dies with the 400 above.
  3. Because the API is stateless (store is not supported), Codex replays the whole history on the next request, including the orphaned function_call. Every following request in that conversation then fails with the same call_id, so the thread is permanently wedged: the only recovery is starting a new conversation.

In our session logs one thread failed three times in a row with an identical call_id (call_01_7nfQ0nYFNRkRLiZZS1xE3774), and the same failure appeared on three separate days, always with an injected developer message sitting between the call and its output.

Expected

function_call / custom_tool_call should be paired with their function_call_output / custom_tool_call_output by call_id anywhere in input, the way the OpenAI Responses API does, instead of requiring positional adjacency.

Suggested fix

When merging input items into the upstream chat messages, do not require the output to be the immediately following item:

  • keep pending tool calls open and flush the assistant tool_calls message together with the matching tool messages only once the outputs have been seen; or
  • when a non-tool item appears while tool calls are pending, buffer/reorder so the call–output pair stays adjacent.

The compatibility page hints at the current assumption: function_call — "Merged into the adjacent assistant message", custom_tool_call — "with call_id pairing validation".

Environment

  • model: deepseek-flash
  • endpoint: POST /v1/responses, non-streaming, store: false
  • client: OpenAI Codex CLI / desktop app with the official DeepSeek Codex integration template
  • #738 — Responses API: reasoning summary events missing (same integration, different bug)
Originally created by @LiTaoHH on GitHub (Sep 20, 2026). Original GitHub issue: https://github.com/deepseek-ai/awesome-deepseek-integration/issues/740 ## Summary `POST /v1/responses` rejects a request whose `input` array contains a `function_call` and its matching `function_call_output` that are not **strictly adjacent**. Any item in between — an extra `message` (role `developer`/`user`) or a `reasoning` item — makes the pairing check fail with: ``` HTTP 400 {"error":{"message":"No tool output found for tool call call_...","type":"invalid_request_error","param":"","code":"invalid_request_error"}} ``` even though the matching `function_call_output` is present later in the same `input` array. The OpenAI Responses API pairs tool calls with their outputs by `call_id` regardless of position, so clients written against that behaviour break here — Codex in particular. ## Evidence Four minimal non-streaming requests to `https://api.deepseek.com/v1/responses`, `model: deepseek-flash`, `max_output_tokens: 16`. They differ only in the position of one extra item. ### A — extra `message` (role `developer`) between call and output → 400 ```json { "model": "deepseek-flash", "store": false, "stream": false, "max_output_tokens": 16, "input": [ {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "say ok"}]}, {"type": "reasoning", "summary": [], "content": [{"type": "reasoning_text", "text": "thinking"}]}, {"type": "function_call", "call_id": "call_A", "name": "exec_command", "arguments": "{\"cmd\":\"echo hi\"}"}, {"type": "message", "role": "developer", "content": [{"type": "input_text", "text": "Team Harness suggested checks: run the project's test suite"}]}, {"type": "function_call_output", "call_id": "call_A", "output": "hi"} ] } ``` ``` 400 {"error":{"message":"No tool output found for tool call call_A.","type":"invalid_request_error","param":"","code":"invalid_request_error"}} ``` ### A2 — extra `reasoning` item between call and output → 400, same error Same array as A, with the 4th item replaced by: ```json {"type": "reasoning", "summary": [], "content": [{"type": "reasoning_text", "text": "thinking"}]} ``` ``` 400 {"error":{"message":"No tool output found for tool call call_A2.","type":"invalid_request_error","param":"","code":"invalid_request_error"}} ``` ### B — control: `function_call` immediately followed by its output → 200 Same array as A with the interleaved item removed. Returns a normal response object. ### C — control: the extra item placed *after* the output → 200 ```json {"type": "function_call", "call_id": "call_C", "name": "exec_command", "arguments": "{\"cmd\":\"echo hi\"}"}, {"type": "function_call_output", "call_id": "call_C", "output": "hi"}, {"type": "message", "role": "developer", "content": [{"type": "input_text", "text": "Team Harness suggested checks: run the project's test suite"}]} ``` The failure is therefore purely positional: the tool output is present and correctly addressed by `call_id`, it is only *not adjacent*. Note: a request that omits the `reasoning` item entirely is rejected with `The reasoning_text in the thinking mode must be passed back to the API.` — that appears to be intentional (thinking mode), and the requests above all include the reasoning item, so it is unrelated to this bug. ## Impact on the official Codex integration This is not a corner case for Codex (https://api-docs.deepseek.com/quick_start/agent_integrations/codex): 1. Codex appends non-tool items *inside* a turn — `hookSpecificOutput.additionalContext` returned by `PostToolUse` hooks, agent-harness advisories, and reasoning items land between a tool call and its result. 2. When that happens, the turn dies with the 400 above. 3. Because the API is stateless (`store` is not supported), Codex replays the whole history on the next request, including the orphaned `function_call`. Every following request in that conversation then fails with the *same* `call_id`, so the thread is permanently wedged: the only recovery is starting a new conversation. In our session logs one thread failed three times in a row with an identical `call_id` (`call_01_7nfQ0nYFNRkRLiZZS1xE3774`), and the same failure appeared on three separate days, always with an injected developer message sitting between the call and its output. ## Expected `function_call` / `custom_tool_call` should be paired with their `function_call_output` / `custom_tool_call_output` by `call_id` anywhere in `input`, the way the OpenAI Responses API does, instead of requiring positional adjacency. ## Suggested fix When merging input items into the upstream chat messages, do not require the output to be the immediately following item: - keep pending tool calls open and flush the assistant `tool_calls` message together with the matching tool messages only once the outputs have been seen; or - when a non-tool item appears while tool calls are pending, buffer/reorder so the call–output pair stays adjacent. The compatibility page hints at the current assumption: `function_call` — *"Merged into the adjacent assistant message"*, `custom_tool_call` — *"with call_id pairing validation"*. ## Environment - model: `deepseek-flash` - endpoint: `POST /v1/responses`, non-streaming, `store: false` - client: OpenAI Codex CLI / desktop app with the official DeepSeek Codex integration template ## Related - #738 — Responses API: reasoning summary events missing (same integration, different bug)
Author
Owner

@oymy commented on GitHub (Sep 22, 2026):

One additional case: a normal multi-tool response triggers the same positional failure even without injected developer/user context.

reasoning, message, call1, reasoning, call2, output1, output2  -> 400 for call1
reasoning, message, call1, output1, reasoning, call2, output2  -> 200

Observed with deepseek-harness 0.1.6-alpha.2 using the pi-ai OpenAI Responses adapter. Both tools completed successfully and both outputs had matching call_ids; the continuation failed only when replaying the history.

This matters because Responses clients commonly preserve all calls from one assistant message before appending their results. Until the server pairs by call_id regardless of position, a safe Harness workaround is to split only completed multi-call history and interleave each matched output on the outgoing request, without mutating durable session history.

<!-- gh-comment-id:5771663612 --> @oymy commented on GitHub (Sep 22, 2026): One additional case: a normal multi-tool response triggers the same positional failure even without injected developer/user context. ```text reasoning, message, call1, reasoning, call2, output1, output2 -> 400 for call1 reasoning, message, call1, output1, reasoning, call2, output2 -> 200 ``` Observed with deepseek-harness 0.1.6-alpha.2 using the pi-ai OpenAI Responses adapter. Both tools completed successfully and both outputs had matching `call_id`s; the continuation failed only when replaying the history. This matters because Responses clients commonly preserve all calls from one assistant message before appending their results. Until the server pairs by `call_id` regardless of position, a safe Harness workaround is to split only completed multi-call history and interleave each matched output on the outgoing request, without mutating durable session history.
Author
Owner

@cmzt-coder commented on GitHub (Oct 9, 2026):

Additional data point: the failure is deterministic, and forking a wedged thread does not recover it

Thanks for the four-request positional proof — it matches exactly what I see from the client side. I'm adding session-log evidence from a different Codex build, because two of the operational statements above turned out differently in my sample.

Environment (delta vs. the report above)

  • client: Codex Desktop app 26.1002.52244 (reported cli_version: 0.162.0-alpha.2)
  • model: deepseek-flash, POST /v1/responses, store: false
  • evidence base: 149 local Codex session rollouts, scanned programmatically

1. The mid-turn item is not limited to PostToolUse hooks — PreToolUse does it too

The report above attributes the injected item to PostToolUse hooks. In my sample all four injections came from a PreToolUse hook with matcher: Bash, whose allow path returns hookSpecificOutput.additionalContext. It is persisted as a developer message and lands between the assistant's function_call and its function_call_output.

So whenever a PreToolUse hook accepts a tool call and attaches context, the very next request in that turn is rejected. The repro matrix above is really about "any non-tool item in that position", and PreToolUse output belongs in it as well.

2. The hit rate is 100%, not intermittent

Across the 149 rollouts there are exactly 4 hook-context injections in total, and:

  • 4/4 landed between a function_call and its matching function_call_output;
  • 4/4 killed the turn with the 400 below, in the same request.

Small sample, but the correlation is perfect: whenever the ordering occurs, the failure is immediate. Nothing else in those logs correlates with the error.

3. The damage outlives the failed turn

The illegal ordering is persisted, and because the API is stateless Codex replays it on every later request. Each retry fails again with the same call_id and a new request_id:

  • one thread failed 3 consecutive turns, always naming the same call_id;
  • another failed 2 consecutive turns, same call_id;
  • in total 8 failed turns across 5 threads, all tracing back to only 4 distinct call_ids (one per injection).

4. Forking a wedged thread is not a recovery path

The report says the only recovery is starting a new conversation. In my sample, forking the wedged thread does not help: the fork copies the illegal ordering, so its very first turn failed with the parent thread's call_id. That is how a single injection propagated into a chain of three derived threads.

5. Support for "purely positional": the output is always present

In all four cases the matching function_call_output is in the history, immediately preceded by the injected message. Nothing is missing; only the adjacency is broken. This independently supports the conclusion that the check is positional rather than call_id-based.

6. Adjacency is the discriminator, not tool-call pairing in general

Batched parallel tool calls are everywhere in these logs — sequences of 2–4 function_call items followed by their function_call_outputs — and none of them ever failed. That rules out a general "tool calls are paired incorrectly" explanation and leaves the in-between item as the trigger.

Minimal trace (redacted)

Order and item kinds only; all ids, paths and command contents removed.

order  item
-----  ----------------------------------------------------------------
  1    response_item  function_call        (exec_command, call_id=<CALL>)
  2    response_item  message / developer (content_item_kinds =
                                            ["hooks.additional_context"])
  3    response_item  function_call_output (call_id=<CALL>)
  ...  event_msg       task_complete + error:
                       {"type":"invalid_request_error",
                        "message":"No tool output found for tool call <CALL>."}

(Listed by order, not by log ordinal — the number of unrelated bookkeeping records between these items varies.)

Everything outside those four items is ordinary turn traffic (reasoning, assistant messages, token counts), and the requests in between differ only in request_id.

Ask

Unchanged from the report: pair function_call / custom_tool_call with their outputs by call_id anywhere in input, instead of requiring positional adjacency.

One note for triage: a client-side change on the Codex side (buffering pending tool calls and flushing each call together with its output) would also remove this class of failure, and would protect users of any endpoint that is stricter than the OpenAI Responses spec. That is a separate upstream conversation — flagging it only so the root cause is not closed as client-only.

<!-- gh-comment-id:6072909257 --> @cmzt-coder commented on GitHub (Oct 9, 2026): ### Additional data point: the failure is deterministic, and forking a wedged thread does not recover it Thanks for the four-request positional proof — it matches exactly what I see from the client side. I'm adding session-log evidence from a different Codex build, because two of the operational statements above turned out differently in my sample. **Environment (delta vs. the report above)** * client: Codex Desktop app `26.1002.52244` (reported `cli_version: 0.162.0-alpha.2`) * model: `deepseek-flash`, `POST /v1/responses`, `store: false` * evidence base: 149 local Codex session rollouts, scanned programmatically #### 1. The mid-turn item is not limited to `PostToolUse` hooks — `PreToolUse` does it too The report above attributes the injected item to `PostToolUse` hooks. In my sample all four injections came from a **`PreToolUse` hook with `matcher: Bash`**, whose allow path returns `hookSpecificOutput.additionalContext`. It is persisted as a `developer` message and lands between the assistant's `function_call` and its `function_call_output`. So whenever a `PreToolUse` hook accepts a tool call *and* attaches context, the very next request in that turn is rejected. The repro matrix above is really about "any non-tool item in that position", and `PreToolUse` output belongs in it as well. #### 2. The hit rate is 100%, not intermittent Across the 149 rollouts there are exactly 4 hook-context injections in total, and: * 4/4 landed between a `function_call` and its matching `function_call_output`; * 4/4 killed the turn with the 400 below, in the same request. Small sample, but the correlation is perfect: whenever the ordering occurs, the failure is immediate. Nothing else in those logs correlates with the error. #### 3. The damage outlives the failed turn The illegal ordering is persisted, and because the API is stateless Codex replays it on every later request. Each retry fails again with the **same `call_id`** and a new `request_id`: * one thread failed **3 consecutive turns**, always naming the same `call_id`; * another failed **2 consecutive turns**, same `call_id`; * in total **8 failed turns across 5 threads**, all tracing back to only **4 distinct `call_id`s** (one per injection). #### 4. Forking a wedged thread is not a recovery path The report says the only recovery is starting a new conversation. In my sample, *forking* the wedged thread does not help: the fork copies the illegal ordering, so its **very first turn** failed with the **parent thread's `call_id`**. That is how a single injection propagated into a chain of three derived threads. #### 5. Support for "purely positional": the output is always present In all four cases the matching `function_call_output` **is** in the history, immediately preceded by the injected message. Nothing is missing; only the adjacency is broken. This independently supports the conclusion that the check is positional rather than `call_id`-based. #### 6. Adjacency is the discriminator, not tool-call pairing in general Batched parallel tool calls are everywhere in these logs — sequences of 2–4 `function_call` items followed by their `function_call_output`s — and **none** of them ever failed. That rules out a general "tool calls are paired incorrectly" explanation and leaves the in-between item as the trigger. #### Minimal trace (redacted) Order and item kinds only; all ids, paths and command contents removed. ``` order item ----- ---------------------------------------------------------------- 1 response_item function_call (exec_command, call_id=<CALL>) 2 response_item message / developer (content_item_kinds = ["hooks.additional_context"]) 3 response_item function_call_output (call_id=<CALL>) ... event_msg task_complete + error: {"type":"invalid_request_error", "message":"No tool output found for tool call <CALL>."} ``` (Listed by order, not by log ordinal — the number of unrelated bookkeeping records between these items varies.) Everything outside those four items is ordinary turn traffic (reasoning, assistant messages, token counts), and the requests in between differ only in `request_id`. #### Ask Unchanged from the report: pair `function_call` / `custom_tool_call` with their outputs by `call_id` anywhere in `input`, instead of requiring positional adjacency. One note for triage: a client-side change on the Codex side (buffering pending tool calls and flushing each call together with its output) would also remove this class of failure, and would protect users of any endpoint that is stricter than the OpenAI Responses spec. That is a separate upstream conversation — flagging it only so the root cause is not closed as client-only.
Sign in to join this conversation.
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
deepseek-ai/awesome-deepseek-integration#207
No description provided.