GPT-5.6 Code Mode omits tool_search from exec, degrading deferred MCP discovery

Open 💬 2 comments Opened Jul 10, 2026 by An-jinu

Summary

GPT-5.6 model metadata selects code_mode_only while supporting deferred tool search. The Code Mode converter currently drops ToolSpec::ToolSearch, so the public exec tool does not expose the dedicated BM25 tools.tool_search(...) discovery primitive.

Deferred tools are still reachable through manual ALL_TOOLS filtering, but GPT-5.6 loses the ranked discovery path that GPT-5.5 Direct mode retains. In practice this reduces proactive selection of specialized deferred MCP tools such as CodeGraph and encourages fallback to generic shell search.

Environment

  • Codex CLI: codex-cli 0.144.0-alpha.4
  • Upstream Codex checked: 656a2d0905c9e0b9bdade1badab07ef6d42ca17c
  • OS: macOS 26.3.1, arm64
  • Model: GPT-5.6 (tool_mode = code_mode_only, supports_search_tool = true)
  • Comparison model: GPT-5.5 (tool_mode = null, Direct mode)

Repository Decision

  • Target repository: openai/codex
  • Why this belongs here: the omission is in the upstream ToolSpec to Code Mode conversion and reproduces independently of the MCP provider.
  • Upstream evidence: current codex-rs/tools/src/code_mode.rs converts ToolSpec::ToolSearch { .. } | ToolSpec::WebSearch { .. } to Vec::new().

Reproduction

  1. Use a model with tool_mode = code_mode_only and supports_search_tool = true (current GPT-5.6 metadata), with at least one searchable deferred MCP namespace.
  2. Start a turn and inspect the nested declarations in the public exec tool.
  3. Observe that deferred MCP metadata is available through ALL_TOOLS, but there is no tools.tool_search(...) declaration.
  4. The converter behavior is also locked by the current upstream unit test:

``bash
cargo test -p codex-tools \
tool_spec_to_code_mode_tool_definition_skips_unsupported_variants \
-- --nocapture
``

It passes by asserting that converting ToolSpec::ToolSearch returns None.

Expected Behavior

When search support causes MCP tools to be deferred, Code Mode should expose tool_search as a nested function inside exec. Individual deferred MCP schemas should remain absent from the initial prompt.

Actual Behavior

The planner correctly creates and registers ToolSearchHandler, but both the Code Mode prompt-definition and runtime-definition paths pass the spec through collect_code_mode_*_tool_definitions. ToolSpec::ToolSearch is discarded there, leaving only the less capable instruction to manually filter ALL_TOOLS by name and description.

Evidence

  • codex-rs/models-manager/models.json: GPT-5.6 Sol/Terra/Luna use code_mode_only with search support; GPT-5.5 does not force Code Mode.
  • codex-rs/core/src/mcp_tool_exposure.rs: searchable MCP tools are intentionally deferred.
  • codex-rs/core/src/tools/spec_plan.rs: the deferred runtimes and ToolSearchHandler are registered before Code Mode conversion.
  • codex-rs/tools/src/code_mode.rs: ToolSpec::ToolSearch is converted to an empty definition list.
  • codex-rs/core/src/tools/code_mode/execute_handler.rs: the same conversion determines which nested tools are executable in V8.
  • Existing integration coverage confirms deferred tools can still be called using ALL_TOOLS.find(...); the defect is specifically the missing ranked search primitive, not total tool unreachability.

Local live A/B evidence: before the patch, a GPT-5.6 repository-graph lookup fell back to rg without invoking CodeGraph. With the patch, the same path invoked codegraph_explore first and completed without an rg fallback.

Related but distinct issues:

  • #31894: Responses Lite does not expose the public exec / Code Mode tool at all.
  • #32086: Responses Lite hides a top-level tool_search inside additional_tools for deferred V1 multi-agent tools.
  • #21503: BM25 exact-name ranking after tool_search is successfully invoked.

Root Cause

The deferred-tool planner assumes the search primitive will remain available when it defers MCP schemas. That assumption is true in Direct mode, but Code Mode treats the hosted ToolSpec::ToolSearch variant as unsupported and removes it from both the generated exec guide and the V8 runtime catalog.

Two adjacent bridges are also needed once the definition is preserved: Code Mode invokes nested functions as ToolPayload::Function, while ToolSearchHandler originally accepts only ToolPayload::ToolSearch; and the search result needs a Code Mode result shape containing the serialized LoadableToolSpec[].

Proposed Fix

  1. Convert ToolSpec::ToolSearch into a Code Mode function definition using its existing description and parameter schema.
  2. Accept the Code Mode ToolPayload::Function form in ToolSearchHandler and parse it as SearchToolCallParams, while preserving the native hosted-tool path.
  3. Return the serialized LoadableToolSpec[] through ToolOutput::code_mode_result.
  4. Keep hosted WebSearch behavior unchanged and keep individual deferred MCP schemas out of the initial exec description.

Reference implementation:

I am sharing the commit as implementation context rather than opening an unsolicited PR. If this approach aligns with the intended architecture, I would be happy to open an invited PR, or the commit can be cherry-picked directly.

Verification Plan

  • Unit: ToolSpec::ToolSearch converts to an augmented Code Mode function definition.
  • Output: Code Mode receives a JSON array of loadable tool specs while the native ToolSearchOutput response remains unchanged.
  • Integration: in CodeModeOnly, exec documents tools.tool_search(...), search returns a non-empty result, and the still-deferred MCP tool remains callable without leaking its schema into the initial prompt.
  • Live: repeat a GPT-5.6 graph-shaped repository query and verify CodeGraph is selected without an rg fallback.

Local verification on the reference commit:

cargo test -p codex-tools tool_spec_to_code_mode_tool_definition_supports_tool_search
1 passed; 0 failed

RUST_MIN_STACK=8388608 cargo test -p codex-core --test all code_mode_only_guides_all_tools_search_and_calls_deferred_app_tools
1 passed; 0 failed

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗