write_stdin / numeric tool args fail when providers emit whole JSON floats (e.g. session_id: 96230.0)

Open 💬 0 comments Opened Jul 16, 2026 by shtse8

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?

  1. Enable unified exec tools (exec_command / write_stdin).
  2. Use a model/provider that serializes whole numbers as floats in tool-call arguments (common with some non-OpenAI providers).
  3. Run a long-running / PTY command via exec_command so a session_id is returned.
  4. Call write_stdin with that id as produced by the model (session_id: <id>.0).
  5. 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_commandwrite_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: optional yield_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):

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

View original on GitHub ↗