Skip to content

[Provider compatibility] DeepSeek V4 Flash returns 400 when developer message is interleaved between function_call and function_call_output #1292

Closed

Description

@c3right

Client or integration

Codex App (Windows desktop)

Area

Tools / Responses continuation / provider compatibility

Summary

deepseek/deepseek-v4-flash on the native openai-responses route returns HTTP 400 when Codex sends a continuation whose input history contains a developer message between a function_call and its matching function_call_output.

The tool itself executes successfully, the function_call_output is present, and the call_id matches exactly. The failure is structural/interleaving-related:

function_call(call_id=X)
→ message(role=developer, content=...)
→ function_call_output(call_id=X, output=successful tool result)

Upstream error:

No tool output found for tool call X.

This is reproducible and reversible in one workspace. The developer message content is sourced verbatim from a local task_plan.md context file. Temporarily renaming that file makes the same DeepSeek tool call succeed; restoring it makes the 400 return immediately.

Important nuance: the exact Codex-side mechanism that injects this developer message is not yet identified. We initially suspected a planning-with-files PostToolUse hook, but static inspection ruled that out as the direct source: that hook only emits a short fixed reminder and does not read task_plan.md. The failing request nevertheless contains a developer message matching the beginning of task_plan.md verbatim.

Reproduction

Environment A (failing workspace):

  1. Open a Codex workspace that contains a non-empty task_plan.md and where the current Codex context-injection setup causes part of that file to appear as a developer message in Responses history.
  2. Select deepseek/deepseek-v4-flash with Max effort.
  3. Start a fresh conversation.
  4. Trigger exactly one shell tool call, for example:
Get-Content -Path "D:\AI\storycard\README.md" -TotalCount 5
  1. The first DeepSeek Responses request returns HTTP 200 and emits the tool call.
  2. Codex executes the command successfully.
  3. The follow-up /v1/responses request is sent, but the relevant input order is:
function_call(call_id=X)
message(role=developer, text copied from task_plan.md)
function_call_output(call_id=X, output="Exit code: 0 ...")
  1. DeepSeek returns HTTP 400:
No tool output found for tool call X.

A/B/ABA validation on the same workspace:

  • task_plan.md present: failure reproduced deterministically (5/5 observed failures in prior sessions).
  • Rename only task_plan.md → task_plan.md.disabled, leaving progress.md, findings.md, AGENTS.md, hooks, model, workspace and OpenCodex unchanged: the same single-tool DeepSeek test succeeds.
  • Restore the exact original task_plan.md (SHA256 verified identical): a fresh DeepSeek session immediately reproduces the same 400 again.
  • On the restored failure, logs again show function_call → developer message from task_plan.md → function_call_output, with identical call IDs and a real successful tool result.

Control workspace:

  • Same machine, same DeepSeek model, same OpenCodex, but an empty workspace with no task_plan.md: 28/28 observed tool-result continuation requests returned HTTP 200, with 0 cases where a developer message appeared between a function_call and its function_call_output.

Model control:

  • In the failing workspace, GLM/glm-5.2 executes the same tool successfully even when developer messages are interleaved between function calls and outputs. So this appears specific to the DeepSeek Responses compatibility path, not the shell command or workspace permissions.

Version

Known reproduction: OpenCodex 2.10.2.

The issue still reproduced immediately after upgrading OpenCodex on 2026-08-08. The latest published release at report time is v2.11.0; the exact post-upgrade local ocx --version output was not captured before filing, so please treat 2.10.2 as the exact confirmed version and v2.11.0 as the likely/current upgraded build pending confirmation.

Codex App on the reproducing machine:

  • About version: 26.730.61639 (released 2026-08-05)
  • Windows package: OpenAI.Codex 26.730.8199.0

Operating system

Windows 11 LTSC

PowerShell 7.6.3

Provider and model

Official DeepSeek provider

  • Routed model: deepseek/deepseek-v4-flash
  • Adapter: openai-responses
  • Inbound protocol: responses
  • Requested effort: max

Logs or error output

Representative failing follow-up request:

{
  "model": "deepseek-v4-flash",
  "provider": "deepseek",
  "inboundProtocol": "responses",
  "requestedModel": "deepseek/deepseek-v4-flash",
  "requestedEffort": "max",
  "status": 400,
  "errorCode": "invalid_request_error",
  "upstreamError": "No tool output found for tool call call_00_...",
  "attempts": [
    {
      "adapter": "openai-responses",
      "status": 400,
      "sendCount": 1,
      "recoveryKinds": []
    }
  ]
}

The immediately preceding request in the same conversation returned HTTP 200 with non-zero output/reasoning tokens. The tool command then executed successfully. The next request reached OpenCodex and upstream, but returned the 400 above.

Detailed local log inspection confirms:

function_call(call_id=call_00_sp4OLJNqwFpIzzZ8jpDl6521)
→ message(role=developer, text verbatim from task_plan.md)
→ function_call_output(call_id=call_00_sp4OLJNqwFpIzzZ8jpDl6521, output=successful command result)

The call IDs are identical; the output is not missing.

Screenshots and supporting files

Available if useful. We have:

  • Codex UI screenshots of the tool execution followed by the 400;
  • OpenCodex raw log entries for the preceding HTTP 200 and failing HTTP 400;
  • logs_2.sqlite evidence for the 5/5 failing workspace vs 28/28 successful control workspace;
  • ABA reversible test evidence (task_plan.md present → fail, renamed → pass, restored → fail).

Redacted configuration

The failing machine uses the built-in DeepSeek provider routed through openai-responses. No project-local provider override is involved. The control and failing workspaces share the same global OpenCodex/provider/tool configuration.

No secrets are included in this report.

The workspace has a planning-with-files setup, but inspection showed its PostToolUse hook is not the direct source of the full task_plan.md developer message. The exact Codex-side injection mechanism remains unknown.

Expected behavior

OpenCodex's DeepSeek Responses compatibility path should correctly handle Codex histories where a developer/system message is interleaved between a function_call and the matching function_call_output, or normalize the history into a provider-compatible ordering before forwarding upstream.

At minimum, a valid matching function_call_output with the same call_id and successful tool result should not be treated as missing solely because another message appears between the pair.

Actual behavior

The follow-up request reaches DeepSeek but returns HTTP 400:

No tool output found for tool call <call_id>

This makes DeepSeek unusable as a tool-using executor in affected Codex workspaces while GLM and other paths continue to work.

Checks

  • I searched existing issues and documentation.
  • I removed secrets, tokens, credentials, and private data.
  • I reproduced the issue in fresh DeepSeek conversations.
  • I verified the tool command itself succeeds.
  • I verified function_call and function_call_output use the same call_id.
  • I performed a reversible single-variable A/B/ABA test.
  • I compared against a clean workspace and another routed model.

References

Related but distinct from #875 (DeepSeek V4 Flash Responses route stalls after tool calls). In this report the follow-up request is sent and reaches upstream, but returns a concrete 400. This matches the "follow-up is sent and 400s" branch discussed during #875 triage rather than the older "no follow-up request is sent" failure mode.

If maintainers want, I can provide a sanitized local capture of the exact continuation input around the function_call / developer message / function_call_output triplet.

Activity

  1. github-actions commented on Aug 8, 2026

    @github-actions
    Contributor

    Automated translation bookkeeping — detected language: English.

  2. added
    bugSomething isn't working
    toolstool_calls, MCP, web-search / sidecar tools
    on Aug 8, 2026
  3. lidge-jun commented on Aug 8, 2026

    @lidge-jun
    Owner

    This is one of the better-evidenced reports this repo has had — the ABA with a SHA-verified restore, the 28/28 control workspace, and the GLM cross-model control between them rule out most of what I would otherwise have had to ask you to check. Thank you for doing that work up front.

    Confirming the mechanism on the proxy side, and where I think the fix belongs.

    Why OpenCodex doesn't currently repair this. repairOrphanedInputItems (src/adapters/openai-responses.ts:503) is the pass that fixes tool-pair problems before forwarding. It builds a set of call_ids from function_call/local_shell_call items, then rewrites any function_call_output whose id is not in that set into a user message. It only ever asks "is the pair present?" — never "is the pair contiguous?".

    In your trace the pair is present and the ids match, so that pass returns the input untouched by design, and the interleaved ordering reaches DeepSeek intact:

    function_call(call_id=X)
    message(role=developer, …)      ← survives
    function_call_output(call_id=X)
    

    The same gap explains the model split you observed: nothing in the proxy normalizes ordering, so tolerance is entirely the upstream parser's. GLM accepts the interleaving; DeepSeek's native Responses parser requires the output to follow its call, and reports it as No tool output found for tool call X.

    So the proxy-side fix is ordering normalization, not pair repair: move a function_call_output up to immediately follow its matching function_call, or equivalently hoist interleaved non-tool items out from between them. That is bounded and testable, and it fits beside the existing repair passes.

    Two things I want to check before proposing a patch, rather than guessing:

    1. Whether reordering is safe for providers that do tolerate the interleaving — a normalization that changes message order for everyone is a wider blast radius than a compatibility shim needs. It may belong behind the same provider predicate the other repairs use.
    2. Whether the developer message carries content the model needs positioned there. Moving it after the tool output changes what the model sees when, which is a semantic change, not just a wire fix.

    On the Codex-side injection you flagged as unidentified: that part is outside this proxy. But it matters less than it looks — even if the injector is doing something reasonable, DeepSeek 400ing on interleaved-but-valid history is a compatibility gap OpenCodex should absorb, since absorbing exactly this kind of upstream strictness difference is what the adapter layer is for.

    I'll pick this up as its own unit. If you can capture the raw outbound request body from a failing turn (with any file content redacted — it's your plan file), that would confirm the item order at the wire rather than at the log layer.

  4. c3right commented on Aug 8, 2026

    @c3right
    Author

    Additional A/B evidence and version correction

    Correction to the original report: the failing N machine is also on Codex App 26.803.41515 (released 2026-08-07), not 26.730.61639. The failure first reproduced on OpenCodex 2.10.2 and still reproduced after upgrading that same machine to 2.11.0.

    I now also have a second Windows machine (D) that provides a useful successful control:

    • Codex App: 26.803.41515
    • OpenCodex: 2.10.2
    • Workspace contains the planning-with-files trio, including a non-empty task_plan.md
    • deepseek/deepseek-v4-flash Max can execute the same kind of shell tool call successfully

    I inspected the successful DeepSeek continuation request on D. The relevant input order was:

    message(developer, app-context)
    message(user, plugins/AGENTS instructions)
    message(user, test prompt)
    reasoning
    function_call(call_id=X)
    function_call_output(call_id=X, successful Get-Content result)
    

    The function_call and matching function_call_output were adjacent, with identical call_ids, and both requests returned HTTP 200.

    Crucially, D did not contain the extra developer message whose text is copied verbatim from task_plan.md. There was also no [planning-with-files] ACTIVE PLAN block in that successful request.

    So the cross-machine contrast is now:

    N (FAIL): Codex 26.803.41515 + OCX 2.10.2/2.11.0
              function_call(X)
              -> developer message(text from task_plan.md)
              -> function_call_output(X)
              -> HTTP 400 "No tool output found..."
    
    D (PASS): Codex 26.803.41515 + OCX 2.10.2
              function_call(X)
              -> function_call_output(X)
              -> HTTP 200
    

    This means task_plan.md merely being present is not sufficient across environments. On N it is a reversible trigger because some Codex-side context-injection path inserts a developer message from that file between the tool call and tool output; on D, with the same Codex build and OCX 2.10.2, that injection does not occur.

    The next diagnostic target is therefore the difference in context-injection behavior/configuration between the two Codex environments, not the DeepSeek model version or OpenCodex 2.10.2 alone.

    The original ABA result on N still holds exactly:

    • task_plan.md present -> developer message interleaved -> DeepSeek 400
    • rename only task_plan.md -> DeepSeek tool continuation succeeds
    • restore identical file -> same developer message reappears between fc/fco -> same 400 returns

    If useful, I can next provide a sanitized comparison of the two machines' ~/.codex hooks/skills/config fingerprints to identify what enables that extra injection on N.

  5. c3right commented on Aug 8, 2026

    @c3right
    Author

    Wire-level confirmation requested by maintainer

    I captured the actual outbound /v1/responses body from a failing N-machine turn at the codex_http_client::transport layer, immediately after compression and before the HTTP 400 response. The JSON is complete and parses successfully, so this is the wire-level body Codex sent to OpenCodex, not a reconstructed summary.

    Sanitized local ordering around the failing tool pair:

    [
      {
        "type": "reasoning",
        "id": "rs_ocx_cdea75996b094351ae6dc1274e26660f_0",
        "summary": "[REDACTED]"
      },
      {
        "type": "function_call",
        "name": "shell_command",
        "call_id": "call_00_sp4OLJNqwFpIzzZ8jpDl6521",
        "arguments": "[REDACTED]"
      },
      {
        "type": "message",
        "role": "developer",
        "content": "[REDACTED_TASK_PLAN_CONTENT]"
      },
      {
        "type": "function_call_output",
        "call_id": "call_00_sp4OLJNqwFpIzzZ8jpDl6521",
        "output": "[REDACTED_TOOL_OUTPUT]"
      }
    ]

    The complete input[] for that request contained 9 items; the final four were exactly the sequence above. The function_call and function_call_output use the identical call_id, and the output contains a real successful tool result (Exit code: 0 + file content) before redaction.

    Transport evidence chain on the client:

    • request compression log: pre-compression 90,754 bytes, post-compression 31,850 bytes
    • codex_http_client::transport: POST http://127.0.0.1:10100/v1/responses with the complete body
    • next client log: 400 Bad Request
    • turn error: No tool output found for tool call call_00_sp4OLJNqwFpIzzZ8jpDl6521

    So the interleaving is confirmed at the actual client->OpenCodex wire boundary:

    function_call(X)
    -> developer message
    -> function_call_output(X)
    

    The developer message text matches the beginning of the workspace task_plan.md verbatim. Content is redacted here.

    Additional model control: GLM 5.2 in the same workspace tolerates analogous interleaving (in observed turns, even many developer messages between call/output) and returns HTTP 200.

    This confirms the ordering gap you described in repairOrphanedInputItems: the pair is present and IDs match, but contiguity is not normalized before DeepSeek sees the request.

  6. c3right commented on Aug 8, 2026

    @c3right
    Author

    Correction to the D-machine control: the reason D did not reproduce the interleaving is now identified. Its planning-with-files hooks were registered/trusted but their Windows commandWindows strings were malformed, so the hooks never actually produced context.

    D had entries like:

    cmd /d /s /c ""C:\Users\kylin\.codex\hooks\pwf-hook.cmd" run_sh.py user-prompt-submit.sh
    

    A read-only launch-chain probe showed:

    • exact D commandWindows -> exit 1, no stdout, no ACTIVE PLAN
    • direct pwf-hook.cmd run_sh.py user-prompt-submit.sh -> exit 0, valid hookSpecificOutput.additionalContext
    • removing only the problematic /c quoting layout makes the hook chain work

    So D was not a valid "same PWF runtime but DeepSeek succeeds" control: its PWF runtime injection was effectively disabled by bad Windows command quoting. This explains why its successful DeepSeek request had adjacent function_call -> function_call_output and no task-plan message.

    The N-machine repro remains the canonical case: PWF hooks are actually effective there, PreToolUse injects head -30 task_plan.md as additionalContext, Codex persists that as a developer message between the matching function call/output, and DeepSeek returns the 400.

    This strengthens the provider-compatibility report rather than weakening it: once the hook is truly active, the interleaved wire shape is reproducible and OpenCodex currently forwards it unchanged.

  7. Ingwannu commented on Aug 9, 2026

    @Ingwannu
    Owner

    The wire-level capture is sufficient and confirms the proxy-side compatibility gap. No additional hook fingerprint comparison is needed for the adapter fix.\n\nI will implement this as provider-scoped ordering normalization for strict Responses upstreams, starting with the affected DeepSeek route rather than changing history for every tolerant provider. For a matched function_call/function_call_output pair, the output will be moved directly after its call; interleaved developer content will be preserved immediately after the output, not dropped. Unmatched/orphan handling will remain under the existing repair path.\n\nThe regression coverage will pin the exact call -> developer message -> matching output shape, verify DeepSeek receives an adjacent pair, and verify tolerant providers remain byte/order unchanged. I will link the dev-targeted PR here after focused and full validation.

  8. Ingwannu commented on Aug 9, 2026

    @Ingwannu
    Owner

    Implementation is now in ready-for-review PR #1334, targeting dev.

    The exact reported wire shape is covered: DeepSeek rewrites call → developer message → matching output into call → matching output → developer message, preserving the injected content. The behavior is capability-gated; a tolerant Responses provider keeps the original order. Duplicate or ambiguous call IDs are not guessed.

    Validation on the exact PR head:

    • TypeScript typecheck passed.
    • 277 focused adapter, replay, config, and registry tests passed.
    • All required GitHub CI checks passed after rerunning one unrelated Bun epoll_ctl test-runner failure; the rerun passed all four Linux shards, macOS, packaging smoke checks, hygiene, and CodeRabbit.

    Review remains requested from @lidge-jun and @Wibias. The PR will not be self-approved or self-merged, and this issue remains open until the fix lands on dev.

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingtoolstool_calls, MCP, web-search / sidecar tools

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions