openai / openai/codex

Standalone injected function_call_output is rejected by strict Responses providers; adding a call_id is not a sufficient repair

Open
#45,450 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

app app-server automations bug custom-model
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 on 127.0.0.1
  • Reproduced from a live automation/heartbeat thread and reproduced again with direct wire probes as tabulated above

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.