Repository navigation
[Provider compatibility] DeepSeek V4 Flash returns 400 when developer message is interleaved between function_call and function_call_output #1292
Description
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):
- Open a Codex workspace that contains a non-empty
task_plan.mdand where the current Codex context-injection setup causes part of that file to appear as a developer message in Responses history. - Select
deepseek/deepseek-v4-flashwith Max effort. - Start a fresh conversation.
- Trigger exactly one shell tool call, for example:
Get-Content -Path "D:\AI\storycard\README.md" -TotalCount 5- The first DeepSeek Responses request returns HTTP 200 and emits the tool call.
- Codex executes the command successfully.
- The follow-up
/v1/responsesrequest 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 ...")
- DeepSeek returns HTTP 400:
No tool output found for tool call X.
A/B/ABA validation on the same workspace:
task_plan.mdpresent: failure reproduced deterministically (5/5 observed failures in prior sessions).- Rename only
task_plan.md→task_plan.md.disabled, leavingprogress.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 afunction_calland itsfunction_call_output.
Model control:
- In the failing workspace,
GLM/glm-5.2executes 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.sqliteevidence for the 5/5 failing workspace vs 28/28 successful control workspace;- ABA reversible test evidence (
task_plan.mdpresent → 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_callandfunction_call_outputuse the samecall_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
github-actions commentedon Aug 8, 2026
Automated translation bookkeeping — detected language: English.
lidge-jun commentedon Aug 8, 2026
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 ofcall_ids fromfunction_call/local_shell_callitems, then rewrites anyfunction_call_outputwhose 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:
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_outputup to immediately follow its matchingfunction_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:
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.
c3right commentedon Aug 8, 2026
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:
task_plan.mddeepseek/deepseek-v4-flashMax can execute the same kind of shell tool call successfullyI inspected the successful DeepSeek continuation request on D. The relevant input order was:
The
function_calland matchingfunction_call_outputwere adjacent, with identicalcall_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 PLANblock in that successful request.So the cross-machine contrast is now:
This means
task_plan.mdmerely 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.mdpresent -> developer message interleaved -> DeepSeek 400task_plan.md-> DeepSeek tool continuation succeedsIf useful, I can next provide a sanitized comparison of the two machines'
~/.codexhooks/skills/config fingerprints to identify what enables that extra injection on N.c3right commentedon Aug 8, 2026
Wire-level confirmation requested by maintainer
I captured the actual outbound
/v1/responsesbody from a failing N-machine turn at thecodex_http_client::transportlayer, 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. Thefunction_callandfunction_call_outputuse the identicalcall_id, and the output contains a real successful tool result (Exit code: 0+ file content) before redaction.Transport evidence chain on the client:
codex_http_client::transport:POST http://127.0.0.1:10100/v1/responseswith the complete body400 Bad RequestNo tool output found for tool call call_00_sp4OLJNqwFpIzzZ8jpDl6521So the interleaving is confirmed at the actual client->OpenCodex wire boundary:
The developer message text matches the beginning of the workspace
task_plan.mdverbatim. 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.c3right commentedon Aug 8, 2026
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
commandWindowsstrings were malformed, so the hooks never actually produced context.D had entries like:
A read-only launch-chain probe showed:
commandWindows-> exit 1, no stdout, no ACTIVE PLANpwf-hook.cmd run_sh.py user-prompt-submit.sh-> exit 0, validhookSpecificOutput.additionalContext/cquoting layout makes the hook chain workSo 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_outputand no task-plan message.The N-machine repro remains the canonical case: PWF hooks are actually effective there,
PreToolUseinjectshead -30 task_plan.mdasadditionalContext, 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.
Ingwannu commentedon Aug 9, 2026
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.
Ingwannu commentedon Aug 9, 2026
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:
epoll_ctltest-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.fix(providers): give Kimi the Responses tool-result adjacency repair (l…
fix(providers): give Kimi the Responses tool-result adjacency repair (l…
fix(xai): seed Responses tool-result adjacency for interrupted Codex …