Code Mode: support function-backed `exec` for providers without custom/freeform tool support
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
- Existing custom/freeform-capable providers continue using the current
execrepresentation. - Function-only providers can select a function-backed
execrepresentation. - Both paths reach the same Code Mode runtime.
- JavaScript execution, nested
tools.*calls, MCP routing, and yielded execution continue to work unchanged. - No model-specific checks such as
if deepseekare 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.
2 Comments
Supporting note from the tool layer: the infrastructure for a dual transport already exists — the registry treats
ToolSpec::FunctionandToolSpec::Freeformuniformly (they share the default-namespace path inspec_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 aFunctionspec 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.
POC update: the proposed function-backed
exectransport works end-to-end.I implemented a local, uncommitted POC with the following shape:
custom_toolsprovider capability;execas the existing freeform tool when supported;code;ToolPayload::CustomandToolPayload::Functionat the existing handler boundary;The function form is:
Validation completed:
execreaches the existing runtime and can call nestedtools.exec_command;deepseek-v4-flash.The live flow completed successfully:
One configuration gotcha found during testing:
excluded_tool_namespaces = ["functions"]also removes default-namespace nested tools such asexec_command,write_stdin, andapply_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.