Skip to content

[code-mode] exec results honour the script's max_output_tokens with no upper bound — one 497 KB shell result reached the model as ~138K tokens (function mode clamps to the model's truncation policy) #42367

Open
argszero/codex
#2

Description

@tmatup

What version of Codex CLI is running?

codex-cli 0.137.0-alpha.4 (app-server, driven through the openai-codex Python SDK 0.1.0b3). The same code is on main today.

Which model were you using?

gpt-5.6-luna (Azure OpenAI deployment; the catalog resolves it to tool_mode: code_mode_only). Also seen with gpt-5.6-terra.

What issue are you seeing?

In code mode, the size of a tool result the model receives is bounded only by what the model itself asked for:

  • codex-rs/core/src/tools/code_mode/mod.rs — truncate_code_mode_result uses resolve_max_tokens(max_output_tokens), i.e. the // @exec: {"max_output_tokens": N} pragma value (or 10 000 when absent), with no clamp;
  • codex-rs/core/src/tools/context.rs — the function-mode path ExecCommandToolOutput::model_output_max_tokens does clamp: resolve_max_tokens(self.max_output_tokens).min(self.truncation_policy.token_budget());
  • codex-rs/code-mode/src/description.rs only checks the pragma is a non-negative safe integer.

So a script that raises the pragma passes a raw shell result of any size (up to the 1 MiB UNIFIED_EXEC_OUTPUT_MAX_BYTES buffer) straight into the context. Observed in an automated evaluation run (2026-09-02): an rg … | head -240 over a repository returned 496 887 bytes in 57 very long lines; the next model request carried 159 055 uncached input tokens (≈138K of them that tool result) and took 1 113 s to be answered against the Azure deployment (the same request shape on 2026-08-31, without a raised budget, was cut to ~10K tokens and answered in 2 s). Across ~680 shell results ≥100 KB in the same evaluation archive (gpt-5.6 models), exactly two reached the model uncut — so this is rare, but when it happens it costs a turn's worth of time and tokens and the harness has no knob: tool_output_token_limit only rewrites model.truncation_policy, which the code-mode result path never reads; the catalog's tool_mode overrides features.code_mode* (session/turn_context.rs), so the harness cannot leave code mode either.

Related but the inverse framing: #33402 (outer exec truncates below nested budgets). This report is about the upper direction.

Expected

Code-mode exec/wait results are clamped the way function-mode results already are — min(requested, truncation_policy.token_budget()) (or a configurable ceiling) — so a model cannot request more tool output than the model's own output policy allows.

Steps to reproduce

  1. Any code-mode model (catalog tool_mode: code_mode_only, e.g. gpt-5.6-*).
  2. Have the model run an exec script whose first line is // @exec: {"max_output_tokens": 200000} and whose body calls exec_command({ cmd: "yes 'x…(8 KB line)' | head -60" }) and text(result.output).
  3. In the rollout JSONL, the custom_tool_call_output carries the full ~480 KB; the next request's input_tokens grows by ~130K.

Activity

  1. added
    bugSomething isn't working
    CLIIssues related to the Codex CLI
    tool-callsIssues related to tool calling
    contextIssues related to context management (including compaction)
    azureIssues related to the Azure-hosted OpenAI models
    on Sep 2, 2026
  2. fscfede-beep commented on Sep 3, 2026

    @fscfede-beep

    I rechecked this against current main at 728cb12fe5794b0c3a8e776fb4994b1650b973a8; the code-mode path is still not clamped by the model truncation policy.

    I have now published the one-file reference implementation from the analysis above:

    The patch:

    • passes the current model TruncationPolicy into all 3 code-mode result paths;
    • uses the same stricter-by-byte_budget() precedence already used by function-mode ExecCommandToolOutput::model_output_policy();
    • preserves a smaller explicit max_output_tokens;
    • adds a focused regression where requested Tokens(100) is clamped by model Tokens(5);
    • keeps the existing text/audio truncation implementation unchanged.

    Local source/diff validation on the exact upstream base:

    • base: 728cb12fe5794b0c3a8e776fb4994b1650b973a8
    • changed files: 1
    • production call sites migrated: 3/3
    • old two-argument production calls remaining: 0
    • git diff --check: PASS
    • working tree after commit/push: clean

    Validation boundary: this Windows host does not currently have cargo / rustc, so I am not claiming Rust compile or unit-test success.

    I also checked docs/contributing.md before attempting an upstream PR. The repository explicitly says external code contributions / pull requests are not accepted and asks community contributors to share root-cause analysis and potential fix approaches through issues instead. I therefore will not open an upstream PR; the fork commit is only a concrete reference implementation for maintainer review if useful.

  3. argszero commented on Sep 3, 2026

    @argszero

    tmatup's root-cause is correct — I implemented and validated the clamp on current main (7eee24ef51).

    The code-mode result path (codex-rs/core/src/tools/code_mode/mod.rs truncate_code_mode_result) used resolve_max_tokens(max_output_tokens) with no clamp, so a script's raised @exec max_output_tokens pragma could push a large shell result into context. The function-mode path already clamps via ExecCommandToolOutput::model_output_policy (min(requested, truncation_policy)).

    The fix threads the model's truncation_policy into truncate_code_mode_result and clamps the requested budget to it — mirroring the function-mode clamp exactly, so a model never receives a tool result larger than its own output policy allows.

    Note: upstream PR creation is blocked for contributors (GraphQL CreatePullRequest), so I've put the validated change on a fork: argszero/codex branch fix/code-mode-clamp-output → PR #2. Happy to hand it off to anyone with write access.

    Validated against current main:

    • cargo clippy -p codex-core --lib: clean
    • codex-core code_mode unit tests: 36/36 pass (existing + new high_max_output_tokens_is_clamped_to_model_policy, which raises the pragma to 10,000 tokens and asserts the result is still clamped to a 25-token model policy)
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

    CLIIssues related to the Codex CLIazureIssues related to the Azure-hosted OpenAI modelsbugSomething isn't workingcontextIssues related to context management (including compaction)performancetool-callsIssues related to tool calling

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions