Skip to content

feat(codex): read saved exec JSON usage#1036

Merged
ryoppippi merged 4 commits into
mainfrom
codex/codex-exec-json-usage
May 19, 2026
Merged

feat(codex): read saved exec JSON usage#1036
ryoppippi merged 4 commits into
mainfrom
codex/codex-exec-json-usage

Conversation

@ryoppippi
Copy link
Copy Markdown
Owner

@ryoppippi ryoppippi commented May 17, 2026

Try This PR

bunx -p https://pkg.pr.new/ryoppippi/ccusage@1036 ccusage codex daily --help

Summary

  • add `CODEX_EXEC_LOG_DIR` for saved `codex exec --json` JSONL usage files
  • parse direct and nested `usage` payloads with model, timestamp, cache, and total token fields
  • document the extra Codex log directory alongside `CODEX_HOME`

Validation

  • `pnpm run format`
  • `pnpm --filter ccusage exec vitest run src/adapter/codex/paths.ts src/adapter/codex/parser.ts src/adapter/codex/index.ts`
  • `pnpm typecheck`
  • `pnpm --filter docs build`
  • `git diff --check`
  • `pnpm run test`

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for reading Codex exec JSONL logs directly from CODEX_HOME locations, alongside session directories.
    • Improved parsing of diverse Codex exec JSON result formats with enhanced timestamp and model name detection.
  • Documentation

    • Updated Codex guide and environment variable documentation with examples of configuring CODEX_HOME for direct JSONL log directories.

Review Change Stack

Add CODEX_EXEC_LOG_DIR so ccusage can include JSONL files captured from codex exec --json alongside normal CODEX_HOME session logs.

The Codex parser now accepts direct and nested usage payloads, keeps model and timestamp metadata when present, and falls back to the file timestamp for saved output lines that do not include one.
@ryoppippi ryoppippi added the enhancement New feature or request label May 17, 2026
@ryoppippi
Copy link
Copy Markdown
Owner Author

@cubic-dev-ai review it

@ryoppippi
Copy link
Copy Markdown
Owner Author

@coderabbitai review it

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 17, 2026

📝 Walkthrough

Walkthrough

This PR extends Codex usage tracking to ingest saved codex exec --json JSONL output files from CODEX_HOME locations alongside existing session directories, using intelligent detection (presence of sessions/ subdir) to distinguish homes from direct JSONL inputs. Includes path discovery, headless exec normalization, visitor refactoring, integration, test coverage, and user documentation.

Changes

Codex headless JSONL usage ingestion via CODEX_HOME paths

Layer / File(s) Summary
Path discovery for usage locations from CODEX_HOME
rust/crates/ccusage/src/codex_loader.rs
New codex_usage_paths() and codex_home_paths() functions resolve and deduplicate CODEX_HOME entries (supporting comma-separated lists and ~ expansion), distinguishing homes (with sessions/ subdir) from direct JSONL directories. load_codex_events_inner iterates over the unified usage path set.
Normalization helpers for headless exec result shapes
rust/crates/ccusage/src/codex_loader.rs
Added codex_model_from_result, usage_from_result, codex_timestamp_from_result, and related helpers to extract and normalize model, usage tokens, and timestamps from multiple nesting paths and representations (model/model_name variants, result/data/response wrappers, token counts as numbers or strings). Extended json_u64 to parse string-encoded numbers and added file_modified_timestamp for filesystem-based fallback timestamps.
Refactored JSONL parsing with fallback timestamps and event handler
rust/crates/ccusage/src/codex_loader.rs
visit_codex_session_file now derives fallback timestamp from file modified time, filters JSONL lines via broader substring matching, and extracts event type from JSON. New add_codex_exec_event helper normalizes model, usage, and timestamp using the new helpers and emits CodexTokenUsageEvent.
Integration into loader and adapter
rust/crates/ccusage/src/adapter/codex.rs, rust/crates/ccusage/src/main.rs
Adapter's load_groups switches to crate::codex_usage_paths() to aggregate both session and direct JSONL locations. Main.rs re-exports updated to expose codex_usage_paths instead of codex_sessions_paths.
Test coverage for JSONL parsing and event extraction
rust/crates/ccusage/src/codex_loader.rs
Expanded test module with temp-directory utilities and new test cases that write JSONL exec usage files, load them via load_codex_events_from_directory, and validate event counts, session IDs, timestamps, model extraction, token field parsing (including nested model_name), and computed total tokens.
User documentation for CODEX_HOME flexibility
docs/guide/codex/index.md, docs/guide/environment-variables.md, docs/guide/getting-started.md
Documents that CODEX_HOME may be comma-separated, entries with sessions/ subdirectories are treated as Codex homes, and other entries are read directly as JSONL directories supporting saved codex exec --json output. Updates span the Codex guide data source section, environment variable table, and troubleshooting example.

🎯 3 (Moderate) | ⏱️ ~25 minutes

🐰 JSONL logs hop into view,
CODEX_HOME paths grow new roots,
Usage tokens bloom true!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.69% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(codex): read saved exec JSON usage' directly and clearly describes the main functional change: adding support for reading saved Codex exec JSON files to extract usage data.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 codex/codex-exec-json-usage

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 and usage tips.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 17, 2026

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 a182bc8 Commit Preview URL

Branch Preview URL
May 19 2026, 11:42 AM

@cubic-dev-ai
Copy link
Copy Markdown

cubic-dev-ai Bot commented May 17, 2026

@cubic-dev-ai review it

@ryoppippi I have started the AI code review. It will take a few minutes to complete.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 17, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch PR details and diff
gh pr view 1036 --repo ryoppippi/ccusage --json title,body,files,additions,deletions,changedFiles

echo "---FILES---"
gh pr diff 1036 --repo ryoppippi/ccusage

Length of output: 19524


Let me read the full parser file to check for potential double-counting and model extraction issues.


🏁 Script executed:

#!/bin/bash
cat apps/ccusage/src/adapter/codex/parser.ts

Length of output: 28773


@ryoppippi: Here's my review of the PR.


Overview

The feature cleanly adds CODEX_EXEC_LOG_DIR support with flexible nested-payload parsing. The path utilities are well-structured, docs are comprehensive, and the deduplication layer gives protection against cross-directory overlap. A few items worth addressing:


🐛 Bug: model_name at the data/result/response level is silently dropped

extractModelFromResult chains through extractModel(payload.data), but extractModel only checks for model at the payload root — not model_name:

// extractModel only checks:
const fallbackModel = asNonEmptyString(payload.model);   // ✅
// but NOT:
asNonEmptyString(payload.model_name);                    // ❌ missing

The existing test passes only because currentModel is already set from the first entry:

// Entry 2 - data.model_name is the only model field, but extractModelFromResult returns undefined
{ type: 'result', data: { model_name: 'gpt-5.2-codex', ... } }

If a file's first (or only) entry uses model_name inside data/result/response without any prior entry having set currentModel, it silently falls back to LEGACY_FALLBACK_MODEL = 'gpt-5'.

Suggested fix – add model_name lookup in extractModel:

function extractModel(value: unknown): string | undefined {
    const payload = asRecord(value);
    if (payload == null) return undefined;

    const info = asRecord(payload.info);
    if (info != null) {
        const model = asNonEmptyString(info.model) ?? asNonEmptyString(info.model_name);
        if (model != null) return model;
        const metadataModel = asNonEmptyString(asRecord(info.metadata)?.model);
        if (metadataModel != null) return metadataModel;
    }

    return (
        asNonEmptyString(payload.model) ??
        asNonEmptyString(payload.model_name) ?? // ← add this
        asNonEmptyString(asRecord(payload.metadata)?.model)
    );
}

And add a test covering this path:

it('extracts model from model_name when it is the only model indicator', async () => {
    await using fixture = await createFixture({
        exec: {
            'solo.jsonl': JSON.stringify({
                type: 'result',
                data: {
                    timestamp: '2026-03-01T00:00:00.000Z',
                    model_name: 'gpt-5.2-codex',
                    usage: { input_tokens: 10, output_tokens: 5, total_tokens: 15 },
                },
            }),
        },
    });
    vi.stubEnv('CODEX_HOME', '');
    vi.stubEnv('CODEX_EXEC_LOG_DIR', fixture.getPath('exec'));

    await expect(loadTokenUsageEvents()).resolves.toMatchObject([
        { model: 'gpt-5.2-codex', inputTokens: 10 },
    ]);
});

⚡ Performance: overly broad JSONL markers

The new markers '"usage"', '"input_tokens"', and '"prompt_tokens"' are substring matches on raw line text. In a typical session JSONL file, assistant/user messages could contain these strings in their content (e.g. a reply that says "usage": 30%), causing those lines to pass the pre-filter and be needlessly JSON-parsed.

Consider tightening the markers to require the key-colon pattern:

const CODEX_JSONL_MARKERS = [
    'turn_context',
    '"type":"token_count"',
    '"type": "token_count"',
-   '"usage"',
-   '"input_tokens"',
-   '"prompt_tokens"',
+   '"usage":{',
+   '"usage": {',
+   '"input_tokens":',
+   '"prompt_tokens":',
];

This still catches all real headless usage payloads while greatly reducing false positives in conversation content.


🔍 Minor: addHeadlessUsageEvent is called before the existing guards

addHeadlessUsageEvent(entry);          // ← runs for every matched line
if (entryType !== 'event_msg' || timestamp == null) {
    return;
}

In practice this is safe today — normalizeHeadlessUsage returns null for turn_context and other non-usage entries, so no events are emitted. But it means every line reaching the slow JSON-parse path (including turn_context entries) pays the cost of normalizeHeadlessUsage. Moving the call after the turn_context guard would make intent clearer and avoid unnecessary work:

if (entryType === 'turn_context') {
    const model = extractModel(payload);
    if (model != null) { currentModel = model; currentModelIsFallback = false; }
    return;
}
addHeadlessUsageEvent(entry);   // ← only reached for non-turn_context entries
if (entryType !== 'event_msg' || timestamp == null) {
    return;
}

The overall structure is solid and the path/detection changes are backwards-compatible. Addressing the model_name extraction bug would be the main change I'd recommend before merging.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@apps/ccusage/src/adapter/codex/parser.ts`:
- Around line 186-197: extractModelFromResult currently calls extractModel only
on nested objects but extractModel does not inspect top-level model_name, so
records like {data: {model_name, ...}} or {result: {model_name, ...}} are
missed; update extractModelFromResult (or alter extractModel) to explicitly
check for model_name fields on payload and its nested
payload.data/payload.result/payload.response (e.g., payload.model_name,
payload.data.model_name, payload.result.model_name) and return that value before
falling back to calling extractModel, ensuring per-model aggregation picks up
headless result records.
🪄 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

Run ID: 13f8e690-bafb-435c-ad6f-18ee0a22985f

📥 Commits

Reviewing files that changed from the base of the PR and between 4aedac0 and 85f0b33.

📒 Files selected for processing (6)
  • apps/ccusage/src/adapter/codex/parser.ts
  • apps/ccusage/src/adapter/codex/paths.ts
  • docs/guide/codex/index.md
  • docs/guide/environment-variables.md
  • docs/guide/getting-started.md
  • docs/guide/index.md

Comment thread apps/ccusage/src/adapter/codex/parser.ts Outdated
Teach the shared Codex model extractor to read model_name at the root of direct and nested exec JSON payloads.

This prevents saved codex exec usage rows from falling back to the legacy model when model_name is the only model field, and narrows JSONL markers to key-colon matches.
@ryoppippi
Copy link
Copy Markdown
Owner Author

@coderabbitai review it

@ryoppippi
Copy link
Copy Markdown
Owner Author

@cubic-dev-ai review it

@ryoppippi
Copy link
Copy Markdown
Owner Author

@coderabbitai Addressed in e417520 by adding root/nested model_name extraction, covering the single-entry nested model_name case with a regression test, and narrowing the JSONL markers to key-colon matches. Validation: pnpm run format, pnpm --filter ccusage exec vitest run src/adapter/codex/paths.ts src/adapter/codex/parser.ts src/adapter/codex/index.ts, pnpm typecheck, pnpm run test, git diff --check.

@cubic-dev-ai
Copy link
Copy Markdown

cubic-dev-ai Bot commented May 17, 2026

@cubic-dev-ai review it

@ryoppippi I have started the AI code review. It will take a few minutes to complete.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 17, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
gh pr diff 1036 --repo ryoppippi/ccusage | head -400

Length of output: 14937


🏁 Script executed:

#!/bin/bash
git show e417520 --stat
git show e417520 -- apps/ccusage/src/adapter/codex/parser.ts

Length of output: 2987


@ryoppippi: All three items are confirmed resolved in e417520:

  • model_name extractionextractModel now falls back to payload.model_name, and since extractModelFromResult chains through payload.data / .result / .response, the nested case is covered too. ✅
  • Regression test — the 'uses nested model_name when saved codex exec usage has no earlier model' test covers exactly the single-entry, nested-only model_name scenario. ✅
  • JSONL markers — narrowed to '"usage":', '"input_tokens":', and '"prompt_tokens":', eliminating false-positive matches on conversational content. ✅

The minor ordering note about addHeadlessUsageEvent was non-blocking and the current behaviour is correct, so no action needed there. Looks good to merge!


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 17, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the latest diff for the PR
gh pr diff 1036 --repo ryoppippi/ccusage

echo "=== CURRENT PARSER.TS ==="
gh api repos/ryoppippi/ccusage/contents/apps/ccusage/src/adapter/codex/parser.ts?ref=codex/codex-exec-json-usage --jq '.content' | base64 -d

echo "=== CURRENT PATHS.TS ==="
gh api repos/ryoppippi/ccusage/contents/apps/ccusage/src/adapter/codex/paths.ts?ref=codex/codex-exec-json-usage --jq '.content' | base64 -d

echo "=== COMMIT e417520 ==="
gh api repos/ryoppippi/ccusage/commits/e417520 --jq '.files[].filename + " (++" + (.files[].additions|tostring) + " --" + (.files[].deletions|tostring) + ")"'

Length of output: 52088


@ryoppippi: All three issues from the previous review are now resolved in e417520. Here's a quick verification:


model_name bug – Fixed

extractModel now falls back to model_name at the root level:

const fallbackModel = asNonEmptyString(payload.model) ?? asNonEmptyString(payload.model_name);

The 'uses nested model_name when saved codex exec usage has no earlier model' regression test confirms the single-entry nested path works correctly.


addHeadlessUsageEvent ordering – Fixed

turn_context is now handled and returned before addHeadlessUsageEvent is called, so turn_context lines no longer pay the cost of normalizeHeadlessUsage.


✅ JSONL markers – Significantly improved

The markers are now '"usage":', '"input_tokens":', '"prompt_tokens":' (colon-suffixed). One small note: I originally suggested including the opening brace ('"usage":{') to guarantee only object values pass the pre-filter. The current '"usage":' could still in theory match something like "usage":123 in conversation content. That said, this is a very low probability in practice and is a clear improvement over the original bare '"usage"'. Not a blocker at all.


The PR looks solid overall — path utilities are clean, deduplication handles cross-directory overlap, docs are thorough, and the tests cover the new paths well. LGTM 🐇 ✨

[approve]

Replace the inline agent label conditional in the schema generator with a small exhaustive switch helper. This keeps future agent-specific labels out of nested ternaries and makes the config schema label rules easier to extend.
@ryoppippi
Copy link
Copy Markdown
Owner Author

@coderabbitai review it

@ryoppippi
Copy link
Copy Markdown
Owner Author

@cubic-dev-ai review it

@cubic-dev-ai
Copy link
Copy Markdown

cubic-dev-ai Bot commented May 17, 2026

@cubic-dev-ai review it

@ryoppippi I have started the AI code review. It will take a few minutes to complete.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 17, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check current PR status and recent commits
gh pr view 1036 --repo ryoppippi/ccusage --json title,state,headRefName,reviews,commits | jq .

echo "=== RECENT COMMITS ==="
gh api repos/ryoppippi/ccusage/pulls/1036/commits --jq '.[].sha + " " + .[].commit.message'

echo "=== LATEST DIFF ==="
gh pr diff 1036 --repo ryoppippi/ccusage

Length of output: 28433


@ryoppippi: The new commit c22ba12 (refactor(config): clarify agent label selection) looks clean. A quick check:

  • getAgentConfigLabel uses an exhaustive switch over all five AgentName variants (amp, claude, codex, opencode, pi), so TypeScript will surface a type error if a new agent is ever added without updating this helper. The behaviour is identical to the previous ternary.
  • No changes to the parser.ts or paths.ts logic — all three previously flagged issues remain fixed from e417520.

Everything looks good. LGTM 🐇 ✨

[approve]

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 17, 2026

Open in StackBlitz

ccusage

npx https://pkg.pr.new/ryoppippi/ccusage@1036

@ccusage/ccusage-darwin-arm64

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

@ccusage/ccusage-darwin-x64

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

@ccusage/ccusage-linux-arm64

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

@ccusage/ccusage-linux-x64

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

@ccusage/ccusage-win32-arm64

npx https://pkg.pr.new/ryoppippi/ccusage/@ccusage/ccusage-win32-arm64@1036

@ccusage/ccusage-win32-x64

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

commit: a182bc8

@github-actions
Copy link
Copy Markdown
Contributor

ccusage performance comparison

This compares the PR build against the base branch build on the same CI runner.

Committed fixture performance

Committed small fixtures for stable PR-to-PR feedback and explicit Claude/Codex command coverage.

Fixtures: Claude apps/ccusage/test/fixtures/claude (0.00 MiB, 2 files), Codex apps/ccusage/test/fixtures/codex (0.00 MiB, 1 files)
Runtime: package ccusage bin from apps/ccusage/package.json through bun -b, --offline --json, measured by hyperfine with 2 warmups and 7 runs.

Command Input Base median PR median PR vs base Base throughput PR throughput
claude daily --offline --json 0.00 MiB 57.3ms 56.9ms 1.01x 0.03 MiB/s 0.03 MiB/s
claude session --offline --json 0.00 MiB 56.4ms 57.4ms 0.98x 0.03 MiB/s 0.03 MiB/s
codex daily --offline --json 0.00 MiB 57.2ms 57.5ms 1.00x 0.01 MiB/s 0.01 MiB/s
codex session --offline --json 0.00 MiB 57.2ms 57.5ms 0.99x 0.01 MiB/s 0.01 MiB/s

Large real-world-shaped fixture performance

Generated fixtures shaped from aggregate local log statistics: thousands of JSONL files, many small sessions, and a long tail of larger sessions. No real prompts, paths, or outputs are stored in the fixtures.

Fixtures: Claude /home/runner/work/_temp/ccusage-large-fixture (1.01 GiB, 2,597 files), Codex /home/runner/work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2,597 files)
Runtime: package ccusage bin from apps/ccusage/package.json through bun -b, --offline --json, measured by hyperfine with 0 warmups and 1 runs.

Command Input Base median PR median PR vs base Base throughput PR throughput
claude --offline --json 1.01 GiB 1.432s 1.450s 0.99x 720.10 MiB/s 711.16 MiB/s
codex --offline --json 1.01 GiB 1.782s 1.892s 0.94x 578.61 MiB/s 544.96 MiB/s

Package size

Package artifact Base PR Delta Ratio
packed ccusage-*.tgz 67.58 KiB 68.00 KiB +0.42 KiB 0.99x

Lower medians and smaller packed package sizes are better. CI runner noise still applies; use same-run ratios as directional PR feedback, not release guarantees.

Merge the current Rust-based main branch into the Codex exec JSON PR and keep the public configuration model aligned with Claude-style data paths.

Saved codex exec --json output is now discovered through comma-separated CODEX_HOME entries. Entries that contain sessions/ are treated as normal Codex homes, while entries without sessions/ are read directly as JSONL directories, so a command such as CODEX_HOME="/Users/ryoppippi/.codex,/Users/ryoppippi/codex-exec-logs" covers both sources without adding a separate JSON option or CODEX_EXEC_LOG_DIR.

The Rust Codex loader now handles standalone exec usage payloads, nested result usage/model fields, stringified token counts, and missing timestamps by falling back to the source file modification time. Documentation was updated to describe only CODEX_HOME-based discovery.
@ryoppippi
Copy link
Copy Markdown
Owner Author

@coderabbitai review

Updated this PR to merge current main and keep saved Codex exec JSON usage support in the Rust CLI. Per review feedback, this now uses comma-separated CODEX_HOME entries only: Codex homes with sessions/ are scanned normally, and entries without sessions/ are read directly as saved codex exec --json JSONL directories.

Local validation passed:

  • nix develop --command cargo test --manifest-path rust/Cargo.toml --workspace codex
  • nix develop --command pnpm run format
  • nix develop --command pnpm typecheck
  • nix develop --command pnpm run test

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 19, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== RECENT COMMITS ==="
gh api repos/ryoppippi/ccusage/pulls/1036/commits --jq '.[].sha + " " + .[].commit.message'

echo "=== LATEST DIFF ==="
gh pr diff 1036 --repo ryoppippi/ccusage

echo "=== CURRENT PARSER.TS ==="
gh api repos/ryoppippi/ccusage/contents/apps/ccusage/src/adapter/codex/parser.ts?ref=codex/codex-exec-json-usage --jq '.content' | base64 -d

echo "=== CURRENT PATHS.TS ==="
gh api repos/ryoppippi/ccusage/contents/apps/ccusage/src/adapter/codex/paths.ts?ref=codex/codex-exec-json-usage --jq '.content' | base64 -d

Length of output: 28289


@ryoppippi: The redesign looks clean and well-executed. Here's a summary of what I checked:


codex_usage_paths() / codex_home_paths() split — Sound

The two-function split is clean. codex_home_paths() returns raw dirs, then codex_usage_paths() checks for a sessions/ subdirectory at runtime. The default (~/.codex) still resolves to ~/.codex/sessions when that dir exists, and the behavior is strictly better than before for the case where ~/.codex is present but sessions/ is not.

normalize_codex_timestamp epoch heuristic — Correct

The raw > 10_000_000_000 threshold is correct: 10¹⁰ seconds is year 2286, so any realistic Unix timestamp (currently ~1.7 × 10⁹ s) falls below the threshold and gets multiplied by 1000, while millisecond timestamps (~1.7 × 10¹² ms) fall above and are used as-is.

add_codex_exec_event model fallback — Correct

The parsed_model.clone()current_model update → or_else chain → parsed_model_is_missing sequence is logically sound. The final parsed_model_is_missing call is slightly redundant when the or_else fallback fires (since is_fallback_model is already set there), but it's harmless.

json_u64 string extension — Good defensive addition

Handles stringified token counts emitted by some Codex backends.

✅ Test coverage — Solid

loads_saved_codex_exec_json_usage covers turn.completed + nested data.model_name + prompt_tokens/cached_tokens aliases. uses_nested_model_name_for_standalone_exec_usage covers the single-entry nested model_name regression. Both validate session ID, timestamp, model, and all token fields.

✅ Docs — Consistent

All CODEX_EXEC_LOG_DIR references removed; CODEX_HOME description and examples correctly describe the new dual behaviour across codex/index.md, environment-variables.md, and getting-started.md.

No issues. LGTM 🐇 ✨

[approve]

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
rust/crates/ccusage/src/adapter/codex.rs (1)

112-117: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Scope dedupe to a single usage root.

This loop reuses one seen set across all codex_usage_paths(), so two unrelated roots can suppress each other when they emit the same timestamp/model/token tuple. That undercounts multi-root configs. The simplest fix is to create a fresh dedupe set per path unless you also add a stable source identifier to the dedupe key.

Suggested change
 fn load_groups(shared: &SharedArgs, kind: AgentReportKind) -> Result<BTreeMap<String, CodexGroup>> {
     let mut groups = BTreeMap::new();
-    let seen = create_dedupe_shards();
     for path in crate::codex_usage_paths()? {
+        let seen = create_dedupe_shards();
         merge_groups(
             &mut groups,
             load_groups_from_directory_with_dedupe(&path, shared, kind, &seen)?,
         );
     }
     Ok(groups)
 }
🤖 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/crates/ccusage/src/adapter/codex.rs` around lines 112 - 117, The dedupe
set `seen` is currently shared across all roots in the loop over
crate::codex_usage_paths(), causing entries from different usage roots to
suppress each other; to fix, scope dedupe per root by creating a fresh `seen`
(or equivalent HashSet) for each `path` before calling
load_groups_from_directory_with_dedupe(&path, shared, kind, &seen) and pass that
fresh set into load_groups_from_directory_with_dedupe so deduplication is
limited to that usage root (alternatively, include a stable source identifier in
the dedupe key inside load_groups_from_directory_with_dedupe if cross-root
dedupe is desired).
🤖 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.

Outside diff comments:
In `@rust/crates/ccusage/src/adapter/codex.rs`:
- Around line 112-117: The dedupe set `seen` is currently shared across all
roots in the loop over crate::codex_usage_paths(), causing entries from
different usage roots to suppress each other; to fix, scope dedupe per root by
creating a fresh `seen` (or equivalent HashSet) for each `path` before calling
load_groups_from_directory_with_dedupe(&path, shared, kind, &seen) and pass that
fresh set into load_groups_from_directory_with_dedupe so deduplication is
limited to that usage root (alternatively, include a stable source identifier in
the dedupe key inside load_groups_from_directory_with_dedupe if cross-root
dedupe is desired).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 638c0aba-23ce-47c7-99ba-599c27b3e725

📥 Commits

Reviewing files that changed from the base of the PR and between e417520 and a182bc8.

📒 Files selected for processing (6)
  • docs/guide/codex/index.md
  • docs/guide/environment-variables.md
  • docs/guide/getting-started.md
  • rust/crates/ccusage/src/adapter/codex.rs
  • rust/crates/ccusage/src/codex_loader.rs
  • rust/crates/ccusage/src/main.rs
✅ Files skipped from review due to trivial changes (4)
  • rust/crates/ccusage/src/main.rs
  • docs/guide/environment-variables.md
  • docs/guide/getting-started.md
  • docs/guide/codex/index.md

let mut previous_totals: Option<CodexRawUsage> = None;
let mut current_model: Option<String> = None;
let mut current_model_is_fallback = false;
let fallback_timestamp = file_modified_timestamp(path);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Use a per-record fallback timestamp here.

Every timestamp-less exec row in the file inherits the same fallback_timestamp, so two legitimate rows with the same model/token counts collapse in dedupe_codex_events and usage gets undercounted. Please derive the fallback per line/event, or mark synthesized timestamps so dedupe does not treat them as identical.

Also applies to: 250-287

@github-actions
Copy link
Copy Markdown
Contributor

ccusage performance comparison

PR SHA: a182bc86112e
Base SHA: 71de9507d27a

This compares the PR package against the configured base package on the same CI runner.

Package runner startup

Execution setup measures any pre-benchmark package materialization used by the execution benchmark. Bunx temp cache measures one bunx -p <url> ccusage --version run with an empty Bun install cache. Warm reuses that cache and reports the median of repeated runs.

Package SHA Execution setup Bunx temp cache Bunx warm median Warm samples
Base pkg.pr.new 71de9507d27a 488.5ms 692.4ms 32.9ms 3
PR pkg.pr.new a182bc86112e 539.3ms 430.8ms 32.6ms 3

Cached bunx execution performance

Runs the same large fixture through bunx -p <pkg.pr.new URL> ccusage after the Bun install cache has already been populated by the startup measurement. This separates cached package-runner execution from first-fetch package materialization.

Fixtures: Claude /home/runner/work/_temp/ccusage-large-fixture (1.01 GiB, 2,597 files), Codex /home/runner/work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2,597 files)
Base package: 71de9507d27a; PR package: a182bc86112e. Both run through bunx -p <pkg.pr.new URL> ccusage using the warmed Bun install cache from package runner startup, measured by hyperfine with 0 warmups and 1 runs.

Command Input Base median PR median PR vs base Base throughput PR throughput
bunx -p <pkg> ccusage claude --offline --json 1.01 GiB 670.8ms 674.7ms 0.99x 1.50 GiB/s 1.49 GiB/s
bunx -p <pkg> ccusage codex --offline --json 1.01 GiB 829.1ms 839.3ms 0.99x 1.21 GiB/s 1.20 GiB/s

Package runtime diagnostics

Compares the PR package wrapper, the installed native optional dependency binary, and the workspace release binary on the same large fixture. This identifies whether slow package results come from JavaScript wrapper overhead, the published native binary build, or the Rust core itself.

Fixtures: Claude /home/runner/work/_temp/ccusage-large-fixture (1.01 GiB, 2,597 files), Codex /home/runner/work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2,597 files)
All rows run --offline --json, measured by hyperfine with 0 warmups and 1 runs. This isolates wrapper overhead from the installed native optional dependency and the workspace release binary built on the runner.

Command Runtime Input Median Throughput Samples
claude --offline --json Package wrapper 1.01 GiB 664.4ms 1.52 GiB/s 1
claude --offline --json Installed native binary 1.01 GiB 631.4ms 1.59 GiB/s 1
claude --offline --json Workspace release binary 1.01 GiB 700.2ms 1.44 GiB/s 1
codex --offline --json Package wrapper 1.01 GiB 811.3ms 1.24 GiB/s 1
codex --offline --json Installed native binary 1.01 GiB 795.4ms 1.27 GiB/s 1
codex --offline --json Workspace release binary 1.01 GiB 796.8ms 1.26 GiB/s 1

Committed fixture performance

Committed small fixtures for stable PR-to-PR feedback and explicit Claude/Codex command coverage.

Fixtures: Claude apps/ccusage/test/fixtures/claude (0.00 MiB, 2 files), Codex apps/ccusage/test/fixtures/codex (0.00 MiB, 1 files)
Base runs the published ccusage package from pkg.pr.new, installed before measurement; PR runs the published ccusage package from pkg.pr.new, installed before measurement. Both run --offline --json, measured by hyperfine with 2 warmups and 7 runs.

Command Input Base median PR median PR vs base Base throughput PR throughput
claude daily --offline --json 0.00 MiB 30.1ms 28.9ms 1.04x 0.05 MiB/s 0.05 MiB/s
claude session --offline --json 0.00 MiB 29.6ms 29.2ms 1.01x 0.05 MiB/s 0.05 MiB/s
codex daily --offline --json 0.00 MiB 29.6ms 29.5ms 1.00x 0.03 MiB/s 0.03 MiB/s
codex session --offline --json 0.00 MiB 29.5ms 29.6ms 1.00x 0.03 MiB/s 0.03 MiB/s

Large real-world-shaped fixture performance

Generated fixtures shaped from aggregate local log statistics: thousands of JSONL files, many small sessions, and a long tail of larger sessions. No real prompts, paths, or outputs are stored in the fixtures.

Fixtures: Claude /home/runner/work/_temp/ccusage-large-fixture (1.01 GiB, 2,597 files), Codex /home/runner/work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2,597 files)
Base runs the published ccusage package from pkg.pr.new, installed before measurement; PR runs the published ccusage package from pkg.pr.new, installed before measurement. Both run --offline --json, measured by hyperfine with 0 warmups and 1 runs.

Command Input Base median PR median PR vs base Base throughput PR throughput
claude --offline --json 1.01 GiB 671.0ms 665.5ms 1.01x 1.50 GiB/s 1.51 GiB/s
codex --offline --json 1.01 GiB 834.6ms 806.6ms 1.03x 1.21 GiB/s 1.25 GiB/s

Artifact size

Artifact Base PR Delta Ratio
packed ccusage-*.tgz 8.71 KiB 8.71 KiB -0.00 KiB 1.00x
installed native package binary 2772.96 KiB 2772.96 KiB +0.00 KiB 1.00x
Rust release binary rust/target/release/ccusage - 2699.68 KiB - -

Lower medians and smaller artifacts are better. CI runner noise still applies; use same-run ratios as directional PR feedback, not release guarantees.

@github-actions
Copy link
Copy Markdown
Contributor

ccusage performance comparison

PR SHA: a182bc86112e
Base SHA: 71de9507d27a

This compares the Rust PR release binary against the configured base package on the same CI runner.

Package runner startup

Execution setup measures any pre-benchmark package materialization used by the execution benchmark. Bunx temp cache measures one bunx -p <url> ccusage --version run with an empty Bun install cache. Warm reuses that cache and reports the median of repeated runs.

Package SHA Execution setup Bunx temp cache Bunx warm median Warm samples
Base pkg.pr.new 71de9507d27a 613.4ms 1.059s 32.0ms 3
PR pkg.pr.new a182bc86112e 712.9ms 462.8ms 33.7ms 3

Cached bunx execution performance

Runs the same large fixture through bunx -p <pkg.pr.new URL> ccusage after the Bun install cache has already been populated by the startup measurement. This separates cached package-runner execution from first-fetch package materialization.

Fixtures: Claude /home/runner/work/_temp/ccusage-large-fixture (1.01 GiB, 2,597 files), Codex /home/runner/work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2,597 files)
Base package: 71de9507d27a; PR package: a182bc86112e. Both run through bunx -p <pkg.pr.new URL> ccusage using the warmed Bun install cache from package runner startup, measured by hyperfine with 0 warmups and 1 runs.

Command Input Base median PR median PR vs base Base throughput PR throughput
bunx -p <pkg> ccusage claude --offline --json 1.01 GiB 672.2ms 668.4ms 1.01x 1.50 GiB/s 1.51 GiB/s
bunx -p <pkg> ccusage codex --offline --json 1.01 GiB 840.1ms 832.6ms 1.01x 1.20 GiB/s 1.21 GiB/s

Package runtime diagnostics

Compares the PR package wrapper, the installed native optional dependency binary, and the workspace release binary on the same large fixture. This identifies whether slow package results come from JavaScript wrapper overhead, the published native binary build, or the Rust core itself.

Fixtures: Claude /home/runner/work/_temp/ccusage-large-fixture (1.01 GiB, 2,597 files), Codex /home/runner/work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2,597 files)
All rows run --offline --json, measured by hyperfine with 0 warmups and 1 runs. This isolates wrapper overhead from the installed native optional dependency and the workspace release binary built on the runner.

Command Runtime Input Median Throughput Samples
claude --offline --json Package wrapper 1.01 GiB 670.2ms 1.50 GiB/s 1
claude --offline --json Installed native binary 1.01 GiB 659.5ms 1.53 GiB/s 1
claude --offline --json Workspace release binary 1.01 GiB 709.2ms 1.42 GiB/s 1
codex --offline --json Package wrapper 1.01 GiB 840.8ms 1.20 GiB/s 1
codex --offline --json Installed native binary 1.01 GiB 809.1ms 1.24 GiB/s 1
codex --offline --json Workspace release binary 1.01 GiB 820.6ms 1.23 GiB/s 1

Committed fixture performance

Committed small fixtures for stable PR-to-PR feedback and explicit Claude/Codex command coverage.

Fixtures: Claude apps/ccusage/test/fixtures/claude (0.00 MiB, 2 files), Codex apps/ccusage/test/fixtures/codex (0.00 MiB, 1 files)
Base runs the published ccusage package from pkg.pr.new, installed before measurement; PR runs rust/target/release/ccusage directly. Both run --offline --json, measured by hyperfine with 2 warmups and 7 runs.

Command Input Base median PR median PR vs base Base throughput PR throughput
claude daily --offline --json 0.00 MiB 29.5ms 3.6ms 8.12x 0.05 MiB/s 0.43 MiB/s
claude session --offline --json 0.00 MiB 29.8ms 3.7ms 7.97x 0.05 MiB/s 0.41 MiB/s
codex daily --offline --json 0.00 MiB 29.9ms 3.4ms 8.80x 0.03 MiB/s 0.25 MiB/s
codex session --offline --json 0.00 MiB 29.2ms 3.3ms 8.77x 0.03 MiB/s 0.26 MiB/s

Large real-world-shaped fixture performance

Generated fixtures shaped from aggregate local log statistics: thousands of JSONL files, many small sessions, and a long tail of larger sessions. No real prompts, paths, or outputs are stored in the fixtures.

Fixtures: Claude /home/runner/work/_temp/ccusage-large-fixture (1.01 GiB, 2,597 files), Codex /home/runner/work/_temp/ccusage-large-codex-fixture (1.01 GiB, 2,597 files)
Base runs the published ccusage package from pkg.pr.new, installed before measurement; PR runs rust/target/release/ccusage directly. Both run --offline --json, measured by hyperfine with 0 warmups and 1 runs.

Command Input Base median PR median PR vs base Base throughput PR throughput
claude --offline --json 1.01 GiB 665.4ms 738.4ms 0.90x 1.51 GiB/s 1.36 GiB/s
codex --offline --json 1.01 GiB 818.2ms 805.1ms 1.02x 1.23 GiB/s 1.25 GiB/s

Artifact size

Artifact Base PR Delta Ratio
packed ccusage-*.tgz 8.71 KiB 8.71 KiB -0.00 KiB 1.00x
installed native package binary 2772.96 KiB 2772.96 KiB +0.00 KiB 1.00x
Rust release binary rust/target/release/ccusage - 2699.68 KiB - -

Lower medians and smaller artifacts are better. CI runner noise still applies; use same-run ratios as directional PR feedback, not release guarantees.

@ryoppippi ryoppippi merged commit edb4392 into main May 19, 2026
37 of 39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant