Skip to content

[Go/Zen] /v1/responses: 400 "No tool output found for tool call X" for a valid item sequence, and the result depends on the interleaved text #54698

Open

Description

@Edward4226

Description

POST https://opencode.ai/zen/go/v1/responses (model deepseek-v4.1-flash) rejects a body in which a non-tool item sits between the two function_call_output items of one assistant step, and names a tool call that does have an output:

{"error":{"message":"No tool output found for tool call call_01_b.","type":"invalid_request_error","param":null,"code":"invalid_request_error"}}

Every call_id in that body has a matching output, so the error text is misleading. The outcome also depends on the interleaved item's text: <image_resize_notice>...</image_resize_notice> is accepted, any other text is rejected - so the gateway appears to string-match that one literal token and treat anything else as a user message that ends the tool-result group.

This matters for real clients: in the Responses API input is a flat item sequence and results are associated by call_id, so a client may legitimately place a non-tool item between two tool outputs. Codex does exactly that - when an inline image is resized it appends an <image_resize_notice> developer message between the image tool output and the other tool output of the same batch.

Steps to reproduce

Two requests to https://opencode.ai/zen/go/v1/responses, identical except for one string. Request 1 returns HTTP 400, request 2 returns HTTP 200.

API=https://opencode.ai/zen/go/v1/responses
KEY=$ZEN_API_KEY           # any Zen/Go key with deepseek-v4.1-flash

# 400: interleaved text is anything other than the literal image_resize_notice token
curl -s -X POST "$API" \
  -H "content-type: application/json" -H "authorization: Bearer $KEY" \
  -H "user-agent: curl/8.4.0" -H "x-opencode-session: repro-1" -d '{
  "model":"deepseek-v4.1-flash","stream":false,"max_output_tokens":16,
  "input":[
    {"type":"message","role":"user","content":[{"type":"input_text","text":"say ok"}]},
    {"type":"function_call","id":"fc_a","call_id":"call_00_a","name":"exec_command","arguments":"{}"},
    {"type":"function_call","id":"fc_b","call_id":"call_01_b","name":"exec_command","arguments":"{}"},
    {"type":"function_call_output","call_id":"call_00_a","output":"ok"},
    {"type":"message","role":"developer","content":[{"type":"input_text","text":"x"}]},
    {"type":"function_call_output","call_id":"call_01_b","output":"ok"}
  ]}'
# -> 400 "No tool output found for tool call call_01_b."

# 200: only the interleaved text changed
#    "text":"<image_resize_notice></image_resize_notice>"

Observed with the interleaved item's text / role (everything else identical):

interleaved item result
<image_resize_notice></image_resize_notice>, role=developer 200
<image_resize_notice>resized</image_resize_notice>, role=developer 200
<image_resize_notice>...resized from 1024x9000 to 233x2048 pixels...</image_resize_notice>, role=developer 200
the same text with role=user / system / assistant 400
x / a plain sentence 400
<system_notice>x</system_notice> / <some_other_tag>x</some_other_tag> 400
image_resize_notice (no tags) / IMAGE_RESIZE_NOTICE / image_resiz_notice (typo) 400

Each case was repeated and is deterministic.

Impact

The 400 arrives before any generation, so the client's whole turn is aborted with no assistant output. In Codex the offending order is persisted in the session, so later turns in that thread fail the same way and the thread becomes unusable. Client-side reports of the same shape: openai/codex#52834 and openai/codex#44604.

Environment

  • Endpoint/model: https://opencode.ai/zen/go/v1/responses, deepseek-v4.1-flash (Go subscription)
  • Client: Codex desktop app 26.1007.21434 (codex-cli 0.162.0-alpha.17.2), Windows 11, wire_api = "responses"; requests pass through a local OpenAI-compatible relay that forwards to Zen
  • Production occurrence: 2026-10-11 17:30:21 UTC; the error string on that request carried upstream request id 01271f1e-0095-4424-9a58-7ccb17b37182

Side observation

Which checks run also depends on how items are grouped: sending the same items with contiguous tool outputs triggers The 'reasoning_text' in the thinking mode must be passed back to the API, while the same items with an interleaved message do not. It looks like the item grouping decides which validation fires, which may be worth looking at together with the above.

Activity

  1. opencode-agent commented on Oct 12, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report and the two-request curl repro. I couldn't run it here: this behaviour is in the hosted Zen/Go gateway and testing it needs a Zen/Go API key, which this environment doesn't have. Without a key the endpoint rejects the request for the missing key before it looks at the input items. A maintainer with Go access will need to check it. Your curl pair and the upstream request id should make that straightforward.

    Related reports about the Zen/Go /v1/responses endpoint with Codex-style clients:

  2. Edward4226 commented on Oct 12, 2026

    @Edward4226
    Author

    Follow-up with (a) the tolerance measured directly against POST https://opencode.ai/zen/go/v1/responses (deepseek-v4.1-flash, User-Agent: curl/8.9.1, streamed and not), and (b) a client-side fix.

    One assistant step, two parallel calls, output_a, one interleaved developer message, output_b — the interleaved item's text is the only thing that changes:

    interleaved item result
    <image_resize_notice>…</image_resize_notice> 200
    <image_resize_notice>x (unclosed), <some_other_tag>x</some_other_tag>, <Image_Resize_Notice>x</Image_Resize_Notice> 400 No tool output found for tool call call_b.
    just a note, role user/system/assistant with the notice text 400 No tool output found for tool call call_b.

    Stable over repeats. So the tool-result group appears to end at any interleaved item whose text isn't that one literal, and the check then reports a call whose output is in the list — the misleading part in the report.

    Worth noting in the other direction: the report's own body (both outputs present, the notice between them) answers 200 at this endpoint, before and after our fix, so that particular 400 isn't reproduced by that body alone — it may be the client's follow-up body being assembled before the batch's last output was in it (openai/codex#44604). What is reproduced on this endpoint is the non-notice interleave above.

    The client side (magpie, the local relay this came through) now normalizes the run before relaying: an item inside a call/result run goes after the run's results. Patch and evidence: yetone/magpie#1752

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions