write_stdin / numeric tool args fail when providers emit whole JSON floats (e.g. session_id: 96230.0)
What version of Codex is running?
codex-cli 0.142.5 (also reproduced against current main tool handlers).
What subscription do you have?
N/A (local CLI + custom OpenAI-compatible Responses provider).
Which model were you using?
Non-OpenAI model via OpenAI-compatible Responses (/v1/responses), product surface still using Codex native tools (exec_command / write_stdin / multi-agent waits).
What platform is your computer?
Linux x86_64
What issue are you seeing?
After exec_command returns a live session_id, a follow-up write_stdin call fails at argument parse time when the model emits a whole float:
{"session_id": 96230.0, "chars": "..."}
Typical failure shape:
invalid type: floating point \`96230.0\`, expected i32
(or equivalent parse error returned to the model as a failed tool call)
The same flow works when the model emits a bare integer:
{"session_id": 96230}
This also affects other numeric tool args advertised as JSON Schema "type": "number" but deserialized as Rust integers (yield_time_ms, max_output_tokens, multi-agent timeout_ms, code-mode wait timeouts).
What steps can reproduce the bug?
- Enable unified exec tools (
exec_command/write_stdin). - Use a model/provider that serializes whole numbers as floats in tool-call arguments (common with some non-OpenAI providers).
- Run a long-running / PTY command via
exec_commandso asession_idis returned. - Call
write_stdinwith that id as produced by the model (session_id: <id>.0). - Observe parse failure before the tool body runs.
Minimal parse repro (current main handlers without whole-number dens):
// WriteStdinArgs { session_id: i32, ... } with default serde
serde_json::from_str(r#"{"session_id":96230.0}"#) // Err: floating point, expected i32
serde_json::from_str(r#"{"session_id":96230}"#) // Ok
What is the expected behavior?
Because the published tool schema uses "type": "number" (not "integer"), finite whole values encoded as floats are valid JSON numbers and should be accepted.
From source on main:
// codex-rs/core/src/tools/handlers/shell_spec.rs — create_write_stdin_tool()
"session_id" => JsonSchema::number(Some(
"Identifier of the running unified exec session.".to_string(),
)),
JsonSchema explicitly supports both number and integer; these tools intentionally choose number (covered by write_stdin_tool_matches_expected_spec).
So the public contract allows 96230.0, but the private Rust args struct rejects it:
struct WriteStdinArgs {
session_id: i32, // default serde is stricter than the advertised schema
...
}
Expected: accept finite whole numbers (integer form, whole float form, and optionally whole numeric strings); still reject true fractions (1.5) and non-finite values.
Additional information
Root-cause hypothesis
This is not a model violating schema, and not an OpenAI Responses wire type that distinguishes int vs float (JSON has a single number type). It is a schema-vs-parser mismatch inside Codex:
| Surface | Contract |
|---------|----------|
| Tool schema to model | "type": "number" |
| Handler deserialize | strict i32 / u64 / i64 |
OpenAI-hosted models often emit bare integers for whole values, so the mismatch is easy to miss. Non-OpenAI providers that emit N.0 expose it immediately—especially on the exec_command → write_stdin path where session_id is required.
Affected tools (same class)
write_stdin:session_id,yield_time_ms,max_output_tokens(high impact; session_id required)exec_command: optionalyield_time_ms,max_output_tokens- multi-agent wait /
wait_agent:timeout_ms - code-mode wait timeouts
Proposed approach (high level)
At the tool-arg boundary only, deserialize “whole numbers” for integer-typed fields:
- accept integer JSON numbers, finite whole floats, whole numeric strings
- reject fractions and non-finite values
- keep schema as
number(no wire/schema change required)
This aligns parser behavior with the already-advertised JSON Schema without accepting non-integral values.
Ready implementation (for invitation)
I have a focused patch ready on a fork branch (rebased onto current openai/codex main, tests included):
- Branch: https://github.com/shtse8/codex/tree/fix/whole-number-tool-args
- Compare: https://github.com/openai/codex/compare/main...shtse8:fix/whole-number-tool-args
- Local verification:
cargo test -p codex-core write_stdin_argspasses - accepts whole floats
- rejects fractional
session_id
Happy to open a PR if a Codex team member invites per docs/contributing.md (external PRs are invitation-only).
Why this is high-impact
- Breaks interactive unified-exec continuation (
write_stdin) for any provider that emits whole floats - Contract is already
"number"; fix is small, localized, and testable - Does not require model changes, schema churn, or gateway-side argument rewriting