Code Mode: support function-backed `exec` for providers without custom/freeform tool support

Open 💬 2 comments Opened Aug 10, 2026 by Zhuchen00123

Summary

Codex Code Mode currently exposes its public exec entrypoint as a custom/freeform tool.

This works with providers that support Codex/OpenAI custom tools, but prevents Code Mode from working with Responses-compatible providers that support ordinary function calling while rejecting type: "custom" / freeform tools.

A provider-capability-backed function transport for Code Mode exec could solve this without introducing a second Code Mode runtime.

Reproduction

Environment:

  • Codex CLI
  • Responses API provider: DeepSeek official endpoint
  • Model: deepseek-v4-flash

Enable Code Mode:

codex -c 'features.code_mode.enable=true'

Prompt:

Use code mode to analyze this project.
Please use JavaScript tool execution to inspect multiple files programmatically.

Observed:

Code Mode is enabled in configuration, but model `deepseek-v4-flash` does not advertise Code Mode support.

Unsupported custom tool: 'exec'. Only 'apply_patch' is supported.

The failure happens at the provider tool schema boundary before the Code Mode runtime is reached.

Current behavior

The current flow is effectively:

model
  -> custom/freeform exec(JavaScript)
  -> CodeModeExecuteHandler
  -> existing V8 runtime

The runtime itself is not the compatibility problem. The provider cannot accept the public representation of exec.

Proposed direction

Add a capability-based Code Mode exec transport:

CodeModeExecTransport:
  Freeform
  Function

For providers supporting custom/freeform tools, keep the existing behavior.

For providers supporting only function tools, expose something like:

{
  "type": "function",
  "name": "exec",
  "parameters": {
    "type": "object",
    "properties": {
      "code": {
        "type": "string"
      }
    },
    "required": ["code"]
  }
}

Then normalize the function payload at the handler boundary:

ToolPayload::Function
  -> extract code
  -> existing CodeModeExecuteHandler
  -> existing V8 execution path

This keeps one Code Mode implementation and only changes the model-facing tool representation.

Motivation

Responses API compatibility does not always mean full support for every OpenAI-specific tool type.

Many providers support:

  • Responses API
  • function calling
  • reasoning/tool use

but not arbitrary custom/freeform tools.

A function-backed Code Mode transport would allow more providers and models to reuse the existing Code Mode runtime without provider-specific hacks.

Potentially affected providers include:

  • DeepSeek
  • local Responses-compatible servers
  • other third-party model providers

Related areas

This appears related to existing provider compatibility discussions:

  • namespace tool flattening for providers without namespace support
  • Code Mode tool conversion
  • provider capability detection
  • custom tool compatibility

The main question is whether exec is intentionally tied to custom/freeform tools or whether it should support multiple wire representations depending on provider capabilities.

Suggested acceptance criteria

  1. Existing custom/freeform-capable providers continue using the current exec representation.
  2. Function-only providers can select a function-backed exec representation.
  3. Both paths reach the same Code Mode runtime.
  4. JavaScript execution, nested tools.* calls, MCP routing, and yielded execution continue to work unchanged.
  5. No model-specific checks such as if deepseek are required.

A small proof of concept should only need changes around the public tool specification and payload normalization, while keeping the existing Code Mode runtime untouched.

View original on GitHub ↗

2 Comments

jdcodes1 · 9 days ago

Supporting note from the tool layer: the infrastructure for a dual transport already exists — the registry treats ToolSpec::Function and ToolSpec::Freeform uniformly (they share the default-namespace path in spec_plan.rs), and Code Mode's payload construction is already kind-dispatched (CodeModeToolKind::Freeform => build_freeform_tool_payload(...)https://github.com/openai/codex/blob/1f41cc5d92/codex-rs/core/src/tools/code_mode/mod.rs#L386). So "function-backed exec" is: pick the spec kind per model capability at registration time (freeform when the model advertises custom-tool support, else a Function spec with one required string property, e.g. {"javascript": "..."}), and accept the function-payload variant in the execute handler where it currently matches only the custom payload. The Code Mode runtime behind the handler is untouched, which is exactly the "no second runtime" property you want.

One wrinkle worth deciding explicitly: with a function transport the JS arrives JSON-escaped, which costs tokens and invites escaping mistakes from weaker models — so keeping freeform as the preferred transport where supported (capability-based selection, not a global switch) is the right default. And the current failure mode ("Unsupported custom tool: 'exec'") should become that fallback rather than an error — it's another case of a capability mismatch surfacing as a hard stop when a degraded-but-working path exists.

Zhuchen00123 · 3 days ago

POC update: the proposed function-backed exec transport works end-to-end.

I implemented a local, uncommitted POC with the following shape:

  • add a custom_tools provider capability;
  • expose Code Mode exec as the existing freeform tool when supported;
  • otherwise expose it as a strict function tool with one required string property, code;
  • normalize both ToolPayload::Custom and ToolPayload::Function at the existing handler boundary;
  • keep the V8 runtime and nested tool routing unchanged.

The function form is:

{
  "type": "function",
  "name": "exec",
  "parameters": {
    "type": "object",
    "properties": {
      "code": { "type": "string" }
    },
    "required": ["code"],
    "additionalProperties": false
  },
  "strict": true
}

Validation completed:

  • provider capability selection;
  • function schema generation;
  • function payload parsing;
  • regression coverage for the existing freeform path;
  • integration test proving that function-backed exec reaches the existing runtime and can call nested tools.exec_command;
  • live test against DeepSeek's official Responses endpoint using deepseek-v4-flash.

The live flow completed successfully:

DeepSeek Responses
-> function_call: exec
-> existing Code Mode runtime
-> tools.exec_command
-> pwd / ls -la in a read-only sandbox
-> function_call_output
-> correct final model response

One configuration gotcha found during testing:
excluded_tool_namespaces = ["functions"] also removes default-namespace nested tools such as exec_command, write_stdin, and apply_patch. Removing that exclusion restored them; this was a test configuration issue rather than a transport/runtime issue.

The POC is intentionally narrow and has not been pushed or opened as a PR. It validates the design and DeepSeek path, but vLLM, Ollama, OpenRouter, and Fireworks still need separate compatibility smoke tests.

The remaining design question is how maintainers want the capability expressed: an explicit provider configuration field, provider capability detection, or a conservative fallback for configured third-party Responses providers.

If this direction looks acceptable, I can clean up the POC into a reviewable PR.