Skip to content

tool_search call on a strict Responses provider fails with 400 missing field call_id on the follow-up request #44779

Open

Description

@liblibKylin

What version of the Codex App are you using (From “About Codex” dialog)?

26.901.51231 (macOS). Bundled Codex core: codex-cli 0.153.4.

What subscription do you have?

No ChatGPT subscription for inference on this profile — the app runs against a third-party Responses-compatible provider: DeepSeek (api.deepseek.com, wire API = responses), model deepseek-flash, API key auth.

What platform is your computer?

macOS (Apple silicon), Darwin 25.x.

What issue are you seeing?

tool_search is unusable on this provider configuration: as soon as the model emits a tool_search call, the follow-up request is rejected with a strict-schema 400, so the turn (and every retry of it) fails before any tool result is produced.

{"error":{"message":"Failed to deserialize the JSON body into the target type: input: missing field `call_id` at line 1 column 73159","type":"invalid_request_error","param":null,"code":"invalid_request_error"}}

Four independent reproductions, all failing in well under one second, with a near-constant offset in the serialized body (which suggests the malformed item sits at a fixed position near the tool_search_call entry):

# prompt reported column
1 ask for two tool_search calls in one turn 73278
2 ask for two tool_search calls in one turn (no plugin mention) 73159
3 ask for a single tool_search call 72894
4 ask for a single tool_search call, explicitly no other tool use 73061

The malformed item never reaches the local rollout: the thread's persisted history contains the user/developer messages and the task_started/task_complete events only, i.e. the item with the missing call_id exists on the wire request that the provider rejects. This matches the client-side nature of the other call_id reports listed below.

This is distinct from the existing call_id issues: all of the ones I could find involve function_call_output injected by automations, or normal-thread resume:

I could not find one that covers "model calls tool_search normally, no automation involved, on a custom Responses provider".

What steps can reproduce the bug?

  1. Configure a custom provider that validates the Responses schema strictly, e.g. DeepSeek with wire_api = "responses".
  2. Start a fresh thread (project or projectless) and send a minimal prompt that leads the model to call tool_search, e.g.:
    Call tool_search once with query "libtv doctor target_project list_projects" and report the namespace you get back. Do not use any other tool.
  3. The first sampling request completes, the model returns the tool_search call, and the follow-up request immediately fails with the error above.
  4. Retrying the turn reproduces it identically.

Session ids (sanitized): 01a08fb4-e274-7d21-aa86-6c2e349a083a, 01a08fb5-0b4d-7371-9d92-c8fc2866d9b3, 01a08fb5-3196-7e70-abf0-44ef611bcce4, 01a08fb7-1b7d-7cd1-85fa-3ebcf65ad83a.

What is the expected behavior?

The tool_search call is serialized with a call_id (as required by the Responses schema), the search result is returned to the model, and the turn proceeds. Alternatively, if tool_search cannot be supported on this provider configuration, the feature should be disabled/omitted instead of emitting a request that can never be accepted.

What is the actual behavior?

Every thread that calls tool_search dies on the first follow-up request with missing field call_id. Because the failure happens during request construction, no tool_search_output is ever produced and no workaround exists inside the thread; starting a new thread only helps until a tool_search call happens again.

Additional information

  • Same Core version with a different provider (deepseek-flash replaced by a ChatGPT-side model in the same app) served tool_search successfully earlier the same day in this install, so the item shape that strict providers reject appears to be provider-specific.
  • Local feature flags report tool_search and tool_search_always_defer_mcp_tools as removed while both are clearly still active in this build, in case that is relevant to routing this report.
  • Happy to attach the four rollout JSONL files (sanitized) or run an instrumented build if that helps isolate where the call_id is dropped.

Activity

  1. added
    bugSomething isn't working
    appIssues related to the Codex desktop app
    custom-modelIssues related to custom model providers (including local models)
    tool-callsIssues related to tool calling
    on Sep 11, 2026
  2. argszero commented on Sep 11, 2026

    @argszero

    Thanks for the careful write-up — the observation that the item is on the wire but never in the rollout is the useful part. One code-level detail narrows this considerably, and I think it changes which item you should be looking at.

    In this build, exactly one item type can serialize with the call_id key absent.

    ResponseItem is a plain internally-tagged serde enum (codex-rs/protocol/src/models.rs:978, #[serde(tag = "type", rename_all = "snake_case")] at :977), so every field is emitted unless it carries skip_serializing_if. Checking each variant that has a call_id, in the version you report (codex-cli 0.153.4):

    variant call_id declaration field attribute
    function_call_output Option<String> (:1086) #[serde(default, skip_serializing_if = "Option::is_none")] (:1085)
    tool_search_call Option<String> (:1064) none
    tool_search_output Option<String> (:1140) none
    custom_tool_call_output String (:1125) none — not optional
    function_call, custom_tool_call String none

    Current main is the same shape, shifted (FunctionCallOutput block at :1101, skip attribute on call_id at :1106; ToolSearchCall.call_id at :1085, ToolSearchOutput.call_id at :1161, both without attributes).

    Two consequences:

    • A tool_search_call or tool_search_output without a call_id serializes as "call_id": null — the key is present. Your provider's error is missing field \call_id`, which a serde deserializer raises only when the key is absent; a present-but-null value produces invalid type: null` instead. So neither tool_search item can produce this exact message.
    • function_call_output is the only variant that omits the key entirely, and only when it has no call_id.

    Although the reported column sits near your tool_search_call entry, the item that can actually yield missing field \call_id` is almost certainly a **function_call_output** — the shape reported in the standalone-output family (#42088, #42067, #41690, #44723, #44519): {"type":"function_call_output","id":"fco_…","name":…,"namespace":…,"output":…}with nocall_id`. The column is a weak locator; the payload is the thing to identify.

    Why such an item is never repaired

    The only production path that builds a FunctionCallOutput with call_id: None is standalone-output injection — turn/start with toolOutput (codex-rs/app-server/src/request_processors/turn_processor.rs:573-582; :593-602 on main) and thread/inject_items. Both are intentional and documented. Nothing downstream repairs them:

    • Prompt::get_formatted_input_for_request (codex-rs/core/src/client_common.rs:56-65) only strips image details.
    • ResponseItem::from(ResponseInputItem::FunctionCallOutput { .. }) always sets call_id: Some(call_id) (models.rs:1838), so this shape can only originate from the injection path.
    • build_responses_request's provider sanitization (codex-rs/core/src/client.rs:976-987; :833 on main) only clears internal_chat_message_metadata_passthrough and encrypted_function_args. And is_openai() is literally name == OPENAI_PROVIDER_NAME (codex-rs/model-provider-info/src/lib.rs:468-470; :546-548 on main) — so a custom wire_api = "responses" provider receives the unmatchable item verbatim and rejects the whole request.

    Worth noting: the repo's own mock deliberately permits this shape — validate_request_body_invariants accepts a function_call_output with an empty call_id as long as it has a non-empty name (codex-rs/core/tests/common/responses.rs:1666, gather_output_ids at :1712-1731; :1506 and :1552-1568 on main). Mock and strict upstreams disagree about the contract, which is how this survives the test suite.

    The decisive datum, if you can get it

    Capture the outgoing body once (point base_url at a local echo proxy, or enable request-body trace logging) and inspect the input[] entry at the failing column:

    • If it is function_call_output with no call_id key, this is the mechanism above, and the fix belongs at request assembly (drop or repair unmatched standalone outputs when the provider requires pairing) or at the injection path — not in tool_search.
    • If it is genuinely tool_search_call / tool_search_output, then the above is incomplete and I'd want to see the raw item, because nothing in the tool_search path can emit an absent call_id key. The tool_search item codex emits is paired by construction: ToolSearchOutput::to_response_item(call_id, …) takes the id from the call it answers (codex-rs/core/src/tools/context.rs:206-221; :216-231 on main).

    Since your follow-up request is rejected before any output is produced, the rollout containing only the user/developer messages and task_started/task_complete is consistent with a failure during request assembly, before the assistant's items are persisted — so the rollout won't tell us which item it was. The outgoing body will.

    I can't reproduce it locally (no strict /responses upstream or DeepSeek key here), so the raw item is the missing piece. If you'd rather not attach a full body, the single input[] entry at the reported column is enough.

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

    appIssues related to the Codex desktop appbugSomething isn't workingcustom-modelIssues related to custom model providers (including local models)tool-callsIssues related to tool calling

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions