Skip to content

[Bug] Grok emits integer tool arguments as floats (120000.0) and routed calls are rejected before execution #1611

Closed

Description

@lidge-jun

Client or integration

Codex CLI

Provider or upstream service

xAI (xai/grok-4.5)

OpenCodex version

dev at 3982dae47 (observed live on the running service, 2.13.0)

Endpoint or capability

/v1/responses routed tool calls — integer tool arguments

Current behaviour

Grok serializes integer tool-call arguments as floats, and OpenCodex forwards the string through untouched. Codex then rejects the call before it ever runs, because its tool schemas declare those fields as integer types.

Every failure looks like this, and the tool never executes:

failed to parse function arguments: invalid type: floating point `120000.0`, expected u64
failed to parse function arguments: invalid type: floating point `29356.0`, expected i32
failed to parse function arguments: invalid type: floating point `12000.0`, expected usize

The .0 is present in the assembled arguments string that reaches the client, not something the client introduces. Extracted from a session rollout:

exec_command -> ["yield_time_ms":120000.0]
exec_command -> ["yield_time_ms":120000.0]
write_stdin  -> ["session_id":29356.0, "yield_time_ms":60000.0]
write_stdin  -> ["session_id":29356.0]

The model's intent is an integer in every case: yield_time_ms is a millisecond budget, and session_id is an exec session handle that was itself issued as 29356. Grok is round-tripping them through a float representation and emitting 29356.0.

This is not a JSON round-trip artifact inside OpenCodex. JSON.stringify(JSON.parse(...)) preserves integers exactly:

$ bun -e 'console.log(JSON.stringify(JSON.parse(String.raw`{"session_id":29356,"yield_time_ms":120000}`)))'
{"session_id":29356,"yield_time_ms":120000}

So the .0 arrives from upstream and survives because nothing on the routed path normalizes it. parseArgsObj in src/bridge.ts:246 only decides whether the arguments parse as an object; it does not reconcile argument values against the declared tool schema, and there is no integer coercion anywhere in src/adapters/ for tool-call arguments.

Expected behaviour

A float-valued argument whose declared schema type is integer should reach the client as an integer when the value is integral (Number.isInteger(120000.0) is true), so the tool call runs.

A non-integral value for an integer field is a genuine upstream error and should stay an error rather than being silently truncated.

Impact

Every affected turn is a hard failure, not a degradation: the tool call is rejected before execution, so the model loses the result and typically retries the same call with the same float. In the captured session this consumed five consecutive tool calls across exec_command and write_stdin without a single one running.

It is also transport-visible only on routed providers. The same Codex client works when the model emits real integers, which is why this reads as a Grok-specific tool-calling failure rather than a client bug.

Minimal redacted request or reproduction

  1. Route Codex CLI through OpenCodex at xai/grok-4.5.
  2. Ask for work that makes the model call a tool with an integer argument — any exec_command where it chooses a yield_time_ms, or any write_stdin reusing a session_id.
  3. Observe the function_call_output for that call.

Observed: failed to parse function arguments: invalid type: floating point 120000.0, expected u64, and the command does not run.

Expected: the call executes.

Scope check

Occurrences in local session rollouts are xai/grok-4.5 only. Five distinct values across two tool names (exec_command, write_stdin) and four Rust integer types (u64, i64, i32, usize), which is consistent with a serializer-level behavior on the provider side rather than one bad tool schema.

I have not verified whether other xAI models or other providers do the same; the sample here is one provider.

Logs or error output

{"type":"function_call_output","output":"failed to parse function arguments: invalid type: floating point `120000.0`, expected u64 at line 1 column 310"}
{"type":"function_call_output","output":"failed to parse function arguments: invalid type: floating point `120000.0`, expected u64 at line 1 column 305"}
{"type":"function_call_output","output":"failed to parse function arguments: invalid type: floating point `29356.0`, expected i32"}

Checks

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, and personal data.

Activity

  1. coderabbitai commented on Aug 13, 2026

    @coderabbitai
    Contributor
    🔗 Related PRs

    #403 - feat(grok): auto-configure Grok Build and harden strict-client compatibility [merged]
    #745 - fix: normalize tool function schemas to always have root type: "object" [open]
    #781 - fix(bridge): cancel open tool calls on terminal error (#765) [open]


    📝 Issue Planner

    Check the box below or use the @coderabbitai plan command to generate an implementation plan and prompts that you can use with your favorite coding assistant.

    • Create Plan

    🧪 Issue enrichment is currently in open beta.

    To disable automatic issue enrichment, add the following to your .coderabbit.yaml:

    issue_enrichment:
      auto_enrich:
        enabled: false

    💬 Have feedback or questions? Drop into our discord!

  2. added
    providerProvider adapters, OpenAI-compat presets, upstream API quirks
    toolstool_calls, MCP, web-search / sidecar tools
    on Aug 13, 2026
  3. added a commit that references this issue on Aug 13, 2026
    027a993
  4. lidge-jun commented on Aug 13, 2026

    @lidge-jun
    OwnerAuthor

    Fixed on dev in 027a993.

    Your diagnosis held up end to end. Confirmed by reading the tree rather than assuming: parseArgsObj only decides whether the payload parses as an object, and rg for coerce/Number.isInteger across src/bridge.ts, src/server/responses/, and src/adapters/ turns up nothing but unrelated HTTP-status checks. Nothing reconciled argument values against the declared schema, so the .0 passed straight through.

    I took the coercion side, for the reason you gave: 120000.0 has exactly one integer reading and the user currently has no recovery path. The boundary is intent — an integral float is a representation artifact and gets repaired; 1.5 in an integer field is a genuine disagreement with the schema and is left alone so it still fails, rather than being truncated into a plausible lie.

    Implementation (src/lib/tool-argument-integers.ts):

    • coerces only when the declared type is integer, including ["integer","null"] unions and anyOf/oneOf/allOf branches
    • never touches number-typed fields, strings, or fields with no declared schema
    • walks nested objects, array items, and local `` into $defs/`definitions`, with cycle and depth guards since provider tool schemas are untrusted input
    • refuses to rewrite a value beyond MAX_SAFE_INTEGER, because a JS round-trip could not represent it exactly and would emit a silently different number
    • returns the original string byte-identical when nothing needed repair

    One thing worth naming beyond the report: both emit paths needed it. closeCurrentToolCall handles streaming and flushToolCall handles buffered, so a streaming-only fix would have left the non-streaming path broken. The schema map is built by reference in buildToolBridgeMaps and rides the same options object that already carries declaredToolNames, so this adds a map lookup rather than new plumbing.

    Verification:

    • 15 focused regressions using the exact values from your report (120000.0, 29356.0), including end-to-end assertions through the real bridge on both paths
    • driven red first: ablating the coercion fails 7 of 15, including both wiring tests, so they are not vacuous
    • full suite on Linux: 11551 pass / 0 fail across 719 files, bun run typecheck clean

    On your rg -rn note — you are right that it is not an OpenCodex defect, and thanks for isolating it rather than filing it. Worth remembering that -r is --replace, so -rn means "replace matches with the literal text n".

  5. added a commit that references this issue on Aug 26, 2026
  6. added 3 commits that reference this issue on Sep 17, 2026
    c7b8938
    faa6fcb
    93a873c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingproviderProvider adapters, OpenAI-compat presets, upstream API quirksprovider-compatibilityProvider compatibility reportstoolstool_calls, MCP, web-search / sidecar tools

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions