Standalone injected function_call_output is rejected by strict Responses providers; adding a call_id is not a sufficient repair
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 125k
- Forks
- 19.4k
- PR merge metrics
- PR metrics pending
Description
Summary
Injected standalone function_call_output items (automations, heartbeat ticks, send_message_to_thread / create_thread delegation) carry id / name / namespace / output but no call_id and no matching function_call. Codex supports this shape deliberately (it is treated as external context since #39782 / #39791), and OpenAI-compatible upstreams accept it — but strict Responses providers reject the entire request at the wire layer, and the injected item is persisted into the rollout, so the target thread is bricked from then on. This is the same failure family as #41690, #42088, #45227, #44723, #44519, #42067.
The gap I want to report is not "the field is missing" (that is known) but which repair actually works at the wire level, because the obvious one does not.
Wire-level probes (macOS, app-server 0.154.0-alpha.6.2, provider https://api.deepseek.com, wire_api = "responses")
body sent to /responses |
result |
|---|---|
function_call_output with name + namespace, no call_id |
400 Failed to deserialize the JSON body into the target type: input: missing field 'call_id' |
same item with a synthesized call_id, but still no function_call |
400 No tool call found for tool output with call_id ... |
synthesized function_call + the original output, sharing one call_id |
200 |
the output rewritten into a developer/user message |
200 |
So: assigning a call_id to the orphan is not a fix; the provider requires the pair to exist in the same request. Any proposed normalizer needs to synthesize the call (or convert the item into a message), not just populate the field.
Why this is not simply "stop emitting the item"
The standalone named output is an intentional representation for external tool events. Removing it would change the injection semantics that #39782 / #39791 deliberately introduced. The missing piece is a provider-facing representation step: before serialization, either pair the injected output with a synthesized function_call, or represent it as a message item, for providers that verify pairing.
Related, and probably a different mechanism
This is separate from the tool-batch commit race in #44604 (a function_call whose output has not been appended yet). I reproduced that one too, and interestingly the client-side guard that should cover it already exists: normalize_history calls ensure_call_outputs_present, which inserts an "aborted" output for any call whose call_id has no output (codex-rs/core/src/context_manager/normalize.rs). Still, the race reports keep arriving, which suggests the guard is not applied to every path that reaches the wire — for example the first iteration of a turn uses the caller-provided input directly instead of clone_history().for_prompt(...) (codex-rs/core/src/session/turn.rs:1571). That path is worth auditing alongside the batch barrier.
Environment
- Codex Desktop / app-server
0.154.0-alpha.6.2, macOS arm64 - Provider: DeepSeek official Responses API (
https://api.deepseek.com,wire_api = "responses"), routed through a local gateway on127.0.0.1 - Reproduced from a live automation/heartbeat thread and reproduced again with direct wire probes as tabulated above
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by tracing provider-facing history normalization in codex-rs/core/src/context_manager/normalize.rs, including ensure_call_outputs_present, and compare it with the first-turn path at codex-rs/core/src/session/turn.rs:1571. Use the reported strict Responses wire probes to verify the chosen representation, while preserving standalone external-tool injection semantics and preventing the target thread from being bricked.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- api, backend-api-design
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100