Fix token counting and codex quota window mapping - #59
Merged
Merged
Conversation
Both the Claude and Codex numbers were wrong, in different ways. Claude Code: - Count cache tokens. Cache reads are ~99% of real traffic, so summing only input+output tracked almost nothing (measured 31M vs 4.73B). - De-duplicate usage records on (message.id, requestId). Resumed, branched and compacted sessions rewrite earlier turns into new files; 68% of local usage rows were repeats of an already-counted API call. - Skip `<synthetic>` placeholders, which are local error rows, not usage. - Include subagent/sidechain files, which are real billed usage. - Scan CLAUDE_CONFIG_DIR and ~/.config/claude, not just ~/.claude. Codex: - Map rate-limit windows by window length, never by the primary/secondary slot name. A weekly-only plan reports primary.window_minutes == 10080 with a null secondary, so the slot name painted a 7-day percentage onto the 5-hour ring. - Fix the app-server invocation: `-a untrusted` is no longer a valid approval policy, so the live query exited before answering and every reading silently came from a stale session file. - Sum today's per-turn last_token_usage instead of the final cumulative total, which attributed a whole multi-day session to its last active day (up to 14x inflation locally), and skip repeated identical snapshots emitted on TUI redraw. Shared: - "Today" now starts at local midnight. A UTC boundary discarded the whole morning east of UTC. Adds Tests/quota/test_quota.py (24 cases, stdlib only, temp-dir fixtures, no network or user data) wired into CI, plus the counting contract in dev_docs/integrate/usage-accounting.md.
A rate_limits event that carries only a plan label is not a usable reading, so the freshest-first walk must continue instead of stopping there. sessions_today now uses the local day boundary, matching how tokens are counted (the 24h file window still lets a session that ran past midnight contribute today's events).
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Both the Claude and the Codex numbers were inaccurate, for different reasons. Verified against real local session data.
Codex
primary/secondaryslot nameprimaryfor whichever window is currently authoritative. On a weekly-only plan it reportsprimary.window_minutes == 10080withsecondary == null, so the 7-day figure was painted onto the 5-hour ring.codex -a untrusted app-serveruntrustedwas removed from the approval-policy enum (0.153.x accepts onlyon-request/never), so the CLI exited before answering any JSON-RPC. The live query never worked; every reading silently fell back to a stale session file.tokens_today= final cumulativetotal_token_usageper sessionlast_token_usageover events timestamped today, skipping identical snapshots re-emitted on TUI redraw.Before/after on this machine at the same instant:
used 5h 34%(stale file, wrong ring) becomesused wk 100% (prolite)(live, correct ring).Claude Code
(message.id, requestId), as ccusage does.<synthetic>rows countedsubagents/files skipped~/.claude/projectsreadCLAUDE_CONFIG_DIR(comma-separated) and~/.config/claude.Replaying a real busy day: 290,103 becomes 5,214,460 tokens (18x).
Shared
TODAY_STARTis now local midnight. The UTC boundary meant "today" began at 08:00 local at UTC+8, silently discarding the whole morning.Verification
Tests/quota/test_quota.py: 24 new cases, stdlib only, temp-dir fixtures, no network and no user data. Confirmed to fail against the pre-fix code (11 failures / 9 errors) and pass after. Wired into CI ahead ofswift test.swift buildandswift testgreen (157 tests).quota.py --jsonproduces correct windows for claude, codex and cursor.Counting rules recorded in
dev_docs/integrate/usage-accounting.md.