Skip to content

tool_search exposes deferred tools with an empty schema, permanently breaking threads on strict Responses providers (DeepSeek) #45585

Open

Description

@wenlinl

What happened?

On Codex Desktop with a custom provider that uses wire_api = "responses" (DeepSeek's official endpoint), a thread works normally until the model calls the built-in tool_search tool. From that point on every turn in that thread fails with:

Invalid schema for function 'codex_app::automation_update': schema must be a JSON Schema of 'type: "object"', got 'type: null'.

This looks related to the automation_update schema reports (#37786, #39848, #36441) but the trigger is different: in this build automation_update is deferred (deferLoading: true), it is not part of the initial tool manifest, and the request only starts failing after tool_search returns it. The failure then sticks for the rest of the thread.

Environment

  • Codex Desktop app 26.818.61809, bundled CLI 0.149.0-alpha.4.3
  • macOS 15.6.1 (arm64)
  • model_provider = "deepseek", base_url = "https://api.deepseek.com/", wire_api = "responses", model = "deepseek-v4-flash" with the custom models.json catalog from the DeepSeek integration guide
  • Browser / node_repl MCP server enabled

Steps to reproduce

  1. Use a custom Responses provider (DeepSeek) in the Desktop app.
  2. Let the agent call tool_search with a query that matches a deferred tool — e.g. "automation" (matches codex_app::automation_update), or a query that matches deferred MCP tools such as mcp__node_repl::js.
  3. The very next sampling request fails with the 400 above. All following turns in the same thread fail the same way.

Observed locally: thread A failed at 10:24:08, 10:25:06, 10:25:24, 10:26:52; a second thread failed at 10:27:31 and 10:28:16 — same error every time, always right after a tool_search call.

Evidence

The tool_search result item reports the matched tools with inputSchema: null, for both app tools and MCP tools:

[
  {"type":"namespace","name":"mcp__node_repl","tools":[
    {"name":"js","inputSchema":null},{"name":"js_reset","inputSchema":null}]},
  {"type":"namespace","name":"codex_app","tools":[
    {"name":"automation_update","inputSchema":null},{"name":"open_in_codex","inputSchema":null}]}
]

Minimal payloads sent straight to https://api.deepseek.com/responses:

tool payload result
namespace tool, "parameters": {} the exact error above
namespace tool, "parameters": null accepted
namespace tool, normal object schema accepted
flat function named codex_app::automation_update rejected earlier, for the name pattern — so the flat form is not what is being sent

So the follow-up request appears to carry parameters: {} for tools that were discovered through tool_search, and DeepSeek's schema validation rejects the whole request because of it. Note that automation_update is only the first reported tool because the namespace is validated in order; the same request also contains schema-less MCP tools.

Expected behavior

Tools surfaced by tool_search should carry their real JSON schema, or at minimum a valid placeholder such as {"type": "object", "properties": {}}, so that strict OpenAI-compatible providers accept the request.

Impact

  • One tool_search call permanently bricks the thread on strict providers: every later turn fails before the model can answer.
  • Not recoverable from the prompt side; the user has to abandon the thread.
  • Affects any custom Responses provider that validates tool schemas (DeepSeek, and presumably other strict OpenAI-compatible endpoints).

Workaround

A local proxy in front of the provider that rewrites an empty parameters to {"type": "object", "properties": {}, "additionalProperties": true} makes the identical request succeed (validated end-to-end, including streaming), which confirms the empty schema is the only problem. That is a client-side workaround, not a fix.

Related

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 15, 2026
  2. wenlinl commented on Sep 15, 2026

    @wenlinl
    Author

    Correction after more digging on the current build (Desktop 26.818.61809, CLI 0.149.0-alpha.4.3): the schema that reaches the provider is not empty — it is the app's real Zod-generated automation_update schema (draft 2020-12), whose root is oneOf + $defs with no root type — i.e. the same shape as #37786.

    What is specific to this report is the delivery path: on this build automation_update is deferred (deferLoading: true), so it never appears in the initial tool manifest; the request only breaks once tool_search returns it, and from then on the thread stays broken on every turn.

    Payload matrix against https://api.deepseek.com/responses (namespace tool codex_app / automation_update, defer_loading: true):

    parameters result
    the real app schema as-is (root oneOf, no type) 400 schema must be a JSON Schema of 'type: "object"', got 'type: null'
    same schema + root "type": "object" (oneOf/$defs untouched) 200
    same schema + oneOf→anyOf + root type 200
    {} 400 (same message)
    {"type":"object","properties":{}} 200

    So the minimal fix on the client side is to ensure the root of a function tool schema is an object schema — adding "type": "object" is sufficient; nested oneOf, $ref and $defs are accepted by DeepSeek. A local proxy that does exactly this (plus a placeholder for truly empty schemas) makes all of the above work, which is how I confirmed it.

  3. wenlinl commented on Sep 15, 2026

    @wenlinl
    Author

    Update: root cause confirmed on this side, with a validated client-side workaround.

    Environment. Codex Desktop 26.818.61809 / CLI 0.149.0-alpha.4.3, macOS 15.6.1, custom deepseek provider (wire_api = "responses", model = "deepseek-v4-flash").

    What actually breaks. The schema that reaches the provider is the app's real Zod-generated automation_update schema (draft 2020-12): root is oneOf + $defs with no root type. DeepSeek requires the root of a function-tool schema to be an object schema and rejects the entire request otherwise. Because automation_update is deferred on this build, the request only starts failing after tool_search returns the tool, and the thread then stays broken on every later turn.

    Payload matrix (namespace tool codex_app / automation_update, defer_loading: true, against api.deepseek.com/responses):

    parameters result
    real app schema as-is (root oneOf, no type) 400 got 'type: null'
    same schema + root "type": "object" (oneOf/$defs untouched) 200
    same schema + oneOf→anyOf + root type 200
    {} 400 (same message)
    {"type":"object","properties":{}} 200

    Workaround that fixed it here. A loopback proxy in front of the provider base URL that:

    1. walks the whole request payload (top-level tools[], namespaced tools, and tool definitions embedded in conversation items) and adds a root "type": "object" to any tool schema that lacks one, while leaving oneOf/$defs intact; and
    2. as a safety net, retries once with a permissive schema when the upstream still answers Invalid schema for function '<name>'.

    Result. The thread that had failed on six consecutive turns resumes normally. Since applying it: 27 consecutive provider requests returned 200, zero schema errors, no other client change.

    Suggested proper fix. Emit an object root for tool schemas (adding "type": "object" is sufficient; nested oneOf/$ref/$defs are accepted), and/or do not lose the schema when a deferred tool is re-exposed after tool_search.

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