Skip to content

fix(codex): stop double-counting reasoning in derived token totals - #1499

Merged
ryoppippi merged 3 commits into
mainfrom
fix/codex-derived-total-tokens
Jul 27, 2026
Merged

fix(codex): stop double-counting reasoning in derived token totals#1499
ryoppippi merged 3 commits into
mainfrom
fix/codex-derived-total-tokens

Conversation

@ryoppippi

@ryoppippi ryoppippi commented Jul 27, 2026

Copy link
Copy Markdown
Member

Supersedes #1458, keeping @MaxGhenis's commit intact. Their branch could not be updated in place: MaxGhenis/ccusage is a fork of jackcpku/ccusage rather than a direct fork of this repository, so GitHub reports maintainer_can_modify: true but rejects the push.

Problem

Codex reports reasoning_output_tokens as a subset of output_tokens, so total_tokens is input_tokens + output_tokens. The fallback used when a record omits total_tokens added reasoning on top, inflating the reported total.

A saved codex exec --json turn with 100 input, 50 output and 20 reasoning tokens reported 170 instead of 150:

$ CODEX_HOME=... ccusage codex --json --offline | jq .totals.totalTokens
170     # before
150     # after

Costs were never affected, because pricing reads input, cached input and output directly.

Verification

The semantics were checked against real data rather than assumed. Across 170,429 token_count records in a local ~/.codex/sessions, 21,815 carry a nonzero reasoning_output_tokens:

Invariant Matching records
total_tokens == input_tokens + output_tokens 21,815 / 21,815
total_tokens == input_tokens + output_tokens + reasoning_output_tokens 0 / 21,815

docs/guide/codex/index.md already documents the same rule, noting that reasoning tokens are part of the output charge rather than billed separately, so no documentation change is needed — the code now matches the guide.

Rollout session logs always record a usable total (0 of 170,429 records omit it or report zero alongside nonzero components), so this only affects saved exec JSON usage, where OpenAI-style payloads may omit the field.

Changes

  • fix(codex): correct legacy token total fallback (@MaxGhenis) — derive an absent total as input plus output.
  • fix(codex): derive a recorded zero token total the same way — a recorded total_tokens of zero alongside nonzero components means the field is unusable, not that the turn spent nothing, so it takes the same path. Leaving reasoning in that branch kept the inflated sum for exactly the records that cannot be trusted to report a total. Updates the loader.rs assertion that encoded the old value (14 to 13) and pins the derivation from every direction: recorded nonzero total, absent total, recorded zero, OpenAI field spellings, empty usage object.

Testing

  • just test — full suite green, 0 failures
  • just fmt — 0 changed
  • cargo clippy -p ccusage-adapter-codex --all-targets — no warnings

View with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is enabled.


Summary by cubic

Fixes Codex token totals by removing double-counting of reasoning tokens and treating zero totals as absent. Derived totals now use saturating input + output, so saved exec JSON reports correct totals; costs were never affected.

  • Bug Fixes
    • Derive total_tokens as input_tokens + output_tokens when missing or explicitly zero; do not add reasoning tokens.
    • Preserve recorded nonzero totals; accept OpenAI field spellings; keep empty usage at zero.
    • Saturate and lazily compute the derived sum to avoid overflow or panics; add tests and update one loader assertion.

Written for commit 6c5c4f6. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Corrected derived token usage totals to avoid double-counting reasoning tokens when they are already included in output tokens.
    • Improved handling of missing or zero totals by deriving input + output (with saturation to prevent overflow) while preserving any explicitly reported non-zero totals.
    • Updated unit test expectations for saved Codex execution usage data.

MaxGhenis and others added 2 commits July 27, 2026 13:56
Prepared with AI assistance (audit + implementation: gpt-5.6-sol; review: Claude Fable 5); reviewed and tested before filing.

Co-Authored-By: gpt-5.6-sol <codex@openai.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A recorded `total_tokens` of zero alongside nonzero components means the field
is unusable, not that the turn spent nothing, so it belongs on the same path as
an absent total. Keeping reasoning in that branch left the inflated sum in place
for exactly the records that cannot be trusted to report a total.

Codex reports `reasoning_output_tokens` as a subset of `output_tokens`. Verified
against 21815 real `token_count` records carrying nonzero reasoning: every one
satisfies `total_tokens == input_tokens + output_tokens`, and none satisfies
`total_tokens == input_tokens + output_tokens + reasoning_output_tokens`.
`docs/guide/codex/index.md` documents the same rule, noting that reasoning
tokens are part of the output charge rather than billed separately.

Rollout session logs always record a usable total, so this only affects saved
exec JSON usage, where OpenAI-style payloads may omit or zero the field. Costs
were never affected because pricing reads input, cached input and output.

Also cover a recorded nonzero total, the OpenAI field spellings, and an empty
usage object, so the derivation stays pinned from every direction.
Copilot AI review requested due to automatic review settings July 27, 2026 13:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Codex usage deserialization now derives missing or zero totals from input and output tokens without double-counting reasoning tokens. Tests cover the updated behavior, including saturation and saved usage expectations.

Changes

Codex usage total handling

Layer / File(s) Summary
Total token derivation
rust/adapters/codex/src/types.rs
CodexRawUsage derives missing or zero totals from input plus output tokens using saturation while preserving positive recorded totals.
Total token regression coverage
rust/adapters/codex/src/types.rs, rust/adapters/codex/src/loader.rs
Tests cover reasoning-token handling, zero and non-zero totals, alternate field spellings, saturation, empty usage, and the updated saved-event expectation.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • ccusage/ccusage#1458: Both changes update Codex total-token derivation and related regression tests.

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: fixing derived Codex token totals to avoid double-counting reasoning tokens.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/codex-derived-total-tokens

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 27, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
ccusage-guide 6c5c4f6 Commit Preview URL

Branch Preview URL
Jul 27 2026, 01:30 PM

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes — fixes Codex derived token totals by dropping reasoning from the fallback (reasoning is already part of output_tokens) and treating explicit-zero totals the same as missing.

  • Derive total_tokens as input + output — the old input + output + reasoning double-counted because Codex reports reasoning as a subset of output. Verified against real data (21,815 records with reasoning, zero counterexamples).
  • Treat zero total as absent — a recorded zero alongside nonzero components means the field is untrustworthy. The old filter kept zero only when all components were zero; the new filter treats all zeros uniformly, and the leaves_an_empty_usage_total_at_zero test confirms the all-zero case still yields 0.
  • Add 5 focused unit tests covering absent total, explicit zero total, recorded nonzero total, OpenAI field spellings, and empty usage.

Pullfrog  | ⚠️ this action is pinned to a commit SHA, which freezes the cleanup step — switch to @v0 or keep the SHA fresh with Dependabot | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
rust/adapters/codex/src/types.rs (1)

519-530: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Make the recorded-total test exercise the preservation branch.

The fixture’s recorded total (150) equals input + output, so this test would pass even if the implementation ignored the recorded field and always derived the value. Use a distinct positive recorded total and assert that exact value.

Suggested test adjustment
-                "total_tokens": 150
+                "total_tokens": 151
...
-        assert_eq!(usage.total_tokens, 150);
+        assert_eq!(usage.total_tokens, 151);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/adapters/codex/src/types.rs` around lines 519 - 530, Update the
keeps_a_recorded_total test fixture so total_tokens is a positive value
different from input_tokens plus output_tokens, then assert that exact recorded
value to exercise preservation of the serialized total field.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rust/adapters/codex/src/types.rs`:
- Around line 291-297: Update the total_tokens calculation in the surrounding
token-field conversion to defer fallback evaluation and use saturating addition:
retain positive total_tokens values, while invoking input.saturating_add(output)
only when the total is absent or non-positive.

---

Nitpick comments:
In `@rust/adapters/codex/src/types.rs`:
- Around line 519-530: Update the keeps_a_recorded_total test fixture so
total_tokens is a positive value different from input_tokens plus output_tokens,
then assert that exact recorded value to exercise preservation of the serialized
total field.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a041d3ec-cf99-4f5e-b95c-840bff237ccb

📥 Commits

Reviewing files that changed from the base of the PR and between d7fec09 and a991ca8.

📒 Files selected for processing (2)
  • rust/adapters/codex/src/loader.rs
  • rust/adapters/codex/src/types.rs

Comment thread rust/adapters/codex/src/types.rs Outdated
@pkg-pr-new

pkg-pr-new Bot commented Jul 27, 2026

Copy link
Copy Markdown

Open in StackBlitz

ccusage

npx https://pkg.pr.new/ccusage@1499

@ccusage/ccusage-darwin-arm64

npx https://pkg.pr.new/@ccusage/ccusage-darwin-arm64@1499

@ccusage/ccusage-darwin-x64

npx https://pkg.pr.new/@ccusage/ccusage-darwin-x64@1499

@ccusage/ccusage-linux-arm64

npx https://pkg.pr.new/@ccusage/ccusage-linux-arm64@1499

@ccusage/ccusage-linux-x64

npx https://pkg.pr.new/@ccusage/ccusage-linux-x64@1499

@ccusage/ccusage-win32-x64

npx https://pkg.pr.new/@ccusage/ccusage-win32-x64@1499

commit: 6c5c4f6

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread rust/adapters/codex/src/types.rs
Comment thread rust/adapters/codex/src/types.rs Outdated
`unwrap_or` evaluates its argument eagerly, so the sum was computed even when a
recorded total was preserved. A corrupt or hostile log carrying two near-`u64`
component values would then panic under the checked arithmetic of a debug build
and wrap in release. Defer the sum and saturate it.

Also pin the preservation branch properly. The recorded total in
`keeps_a_recorded_total` was `input + output`, so the test passed whether the
value was preserved or re-derived. Use a total that cannot be derived.

Raised by CodeRabbit on #1499.
Copilot AI review requested due to automatic review settings July 27, 2026 13:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

Copy link
Copy Markdown
Contributor

ccusage performance comparison

PR SHA: a991ca882b49
Base SHA: d7fec09f558c

Performance comparison skipped.

Base package URL was not ready before 300.000s. Fixture performance comparison requires a base package when --base-dir is not provided.

Base package: d7fec09f558c

@github-actions

Copy link
Copy Markdown
Contributor

ccusage performance comparison

PR SHA: a991ca882b49
Base SHA: d7fec09f558c

Performance comparison skipped.

Base package URL was not ready before 300.000s. Fixture performance comparison requires a base package when --base-dir is not provided.

Base package: d7fec09f558c

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes — commit 6c5c4f6 addresses review-bot feedback from the prior round: deferred evaluation with saturating_add and a recorded-total test that cannot accidentally pass through derivation.

  • Switch to unwrap_or_else with saturating_add — the inner addition now evaluates lazily and saturates at u64::MAX instead of wrapping (release) or panicking (debug).
  • Strengthen keeps_a_recorded_total — the fixture total is now 151, distinct from the derived input + output of 150, so the test genuinely exercises the preservation branch.
  • Add saturates_a_derived_total_instead_of_overflowing — input of u64::MAX + 5 saturates to u64::MAX.

Pullfrog  | ⚠️ this action is pinned to a commit SHA, which freezes the cleanup step — switch to @v0 or keep the SHA fresh with Dependabot | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@github-actions

Copy link
Copy Markdown
Contributor

ccusage performance comparison

PR SHA: 6c5c4f6136bf
Base SHA: d7fec09f558c

Performance comparison skipped.

Base package URL was not ready before 300.000s. Fixture performance comparison requires a base package when --base-dir is not provided.

Base package: d7fec09f558c

@github-actions

Copy link
Copy Markdown
Contributor

ccusage performance comparison

PR SHA: 6c5c4f6136bf
Base SHA: d7fec09f558c

Performance comparison skipped.

Base package URL was not ready before 300.000s. Fixture performance comparison requires a base package when --base-dir is not provided.

Base package: d7fec09f558c

@ryoppippi
ryoppippi merged commit 24a3ed0 into main Jul 27, 2026
36 checks passed
@ryoppippi
ryoppippi deleted the fix/codex-derived-total-tokens branch July 27, 2026 13:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants