Skip to content

feat(omni): add unified usage aggregation across AI coding assistants#813

Closed
ben-vargas wants to merge 17 commits intoryoppippi:mainfrom
ben-vargas:omni
Closed

feat(omni): add unified usage aggregation across AI coding assistants#813
ben-vargas wants to merge 17 commits intoryoppippi:mainfrom
ben-vargas:omni

Conversation

@ben-vargas
Copy link
Copy Markdown
Contributor

@ben-vargas ben-vargas commented Jan 17, 2026

Summary

Adds @ccusage/omni, a new unified CLI that aggregates usage data from all supported AI coding assistants (Claude Code, Codex, OpenCode, Pi-agent) into a single report.

  • Combines daily, monthly, and session reports across all data sources
  • Normalizes token semantics while preserving source-faithful totals
  • Shows cost-only grand totals (token semantics differ by source)
  • Supports filtering by source, date range, and timezone
  • Includes performance optimizations for date-filtered data loading

Disclaimer: Majority code generated by gpt-5.2-codex high with minimal manual review - feel free to make changes/improvements, take and make your own in another branch, etc. I'm not heavily vested in the code itself, just wanted a way to run/check all harnesses combined.

Changes

New Package: @ccusage/omni

  • src/_normalizers/ - Per-source data normalizers (claude, codex, opencode, pi)
  • src/data-aggregator.ts - Main aggregation logic with parallel loading
  • src/commands/ - CLI commands (daily, monthly, session)
  • Unified types preserving source-faithful token calculations

Required Changes to Existing Packages

  • @ccusage/codex: Added exports for daily-report, monthly-report, session-report, data-loader
  • @ccusage/opencode: Created report builder functions, added exports
  • @ccusage/pi: Added exports for data-loader

Performance Optimizations

  • Added date prefiltering to all loaders to avoid loading unnecessary data
  • Cached DateTimeFormat instances in Codex for faster date/month key generation
  • OpenCode now skips scanning old session directories when date filters apply
  • Deduplicated LiteLLM pricing fetches - Added shared static cache to LiteLLMPricingFetcher so parallel source loaders share a single network request

Usage

# All sources, daily report
npx @ccusage/omni@latest daily

# Specific sources only
npx @ccusage/omni@latest monthly --sources claude,codex

# Last 7 days, session report
npx @ccusage/omni@latest session --days 7

# JSON output
npx @ccusage/omni@latest daily --json

Notes

  • Amp is intentionally excluded from v1 due to schema/billing differences (credits vs subscription)
  • Codex cache values are marked with † to indicate subset-of-input semantics
  • Grand totals show cost only; token totals are per-source (different semantics)
CleanShot 2026-01-17 at 15 22 55@2x

Summary by CodeRabbit

  • New Features

    • Omni CLI: daily, monthly, session reports with JSON/table outputs, compact view, breakdowns, and date formatting.
    • Multi-source aggregation that returns unified per-source/session/month totals.
  • Improvements

    • Since/until date-range filtering with timezone-aware handling and prefiltering.
    • Deduplication and consistent per-entry and per-model cost reporting.
    • More robust pricing fetches with timeout and shared caching; improved table sizing for model columns.
  • Documentation

    • Added CLI documentation and usage guidance.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 17, 2026

📝 Walkthrough

Walkthrough

Adds a new Omni CLI app with per-source normalizers and commands, extracts reusable daily/monthly/session report builders, adds timezone-aware date filtering and deduplication in multiple loaders, implements shared online pricing cache with timeout, and adjusts terminal table sizing and package exports across several apps.

Changes

Cohort / File(s) Summary
Omni app
apps/omni/package.json, apps/omni/src/index.ts, apps/omni/src/run.ts, apps/omni/src/logger.ts, apps/omni/src/_consts.ts, apps/omni/src/_types.ts, apps/omni/src/_normalizers/*, apps/omni/src/commands/*, apps/omni/src/data-aggregator.ts, apps/omni/tsconfig.json, apps/omni/tsdown.config.ts, apps/omni/vitest.config.ts, apps/omni/CLAUDE.md
New Omni CLI: package, entry, run logic, logger, unified types, source constants, per-source normalizers, commands (daily/monthly/session), orchestration (data-aggregator), configs, tests scaffolds, and docs.
Codex: date filtering & reports
apps/codex/package.json, apps/codex/src/data-loader.ts, apps/codex/src/daily-report.ts, apps/codex/src/monthly-report.ts, apps/codex/src/date-utils.ts, apps/codex/src/_types.ts, apps/codex/src/session-report.ts, apps/codex/tsdown.config.ts
Added package exports; LoadOptions extended with since/until/timezone; file-level mtime and per-event date filtering; timezone-aware date utilities with cached formatters; per-model costUSD field added and used in reports.
OpenCode: report builders & loader
apps/opencode/src/data-loader.ts, apps/opencode/src/daily-report.ts, apps/opencode/src/monthly-report.ts, apps/opencode/src/session-report.ts, apps/opencode/src/commands/*, apps/opencode/package.json, apps/opencode/tsdown.config.ts
Extracted buildDailyReport/buildMonthlyReport/buildSessionReport; commands now call these builders; data-loader now supports date-range filtering and per-file pre-filtering; package exports updated.
Pi: mtime pre-filter & exports
apps/pi/src/data-loader.ts, apps/pi/package.json, apps/pi/tsdown.config.ts
Adds filterFilesBySince to pre-filter files by mtime (timezone-aware) before processing; exposes data-loader via package exports and adds build entry.
Pricing: shared online cache & timeout
packages/internal/src/pricing.ts
Adds timeoutMs option and centralized online pricing fetch with shared cache and in-flight deduplication using AbortController; ensures fallback to offline pricing.
Terminal table improvements
packages/terminal/src/table.ts
Detects Models column dynamically, computes multiline-aware cell widths, and applies model-specific minimum widths instead of hard-coded index checks.
Omni CLI formatting helpers
apps/omni/src/commands/_shared.ts
New formatting utilities for source labels, cost summaries, compact model name shortening, and pushing per-model breakdown rows for CLI tables.
Normalizers & unified types
apps/omni/src/_normalizers/claude.ts, .../codex.ts, .../opencode.ts, .../pi.ts, apps/omni/src/_normalizers/index.ts, apps/omni/src/_types.ts
Added per-source normalization functions to map source outputs into unified Unified*Usage shapes and consolidated re-exports; new unified types module defines cross-source shapes.
ESLint / repo configs
eslint.config.js
Added OMNI_PLAN.md to ESLint ignore patterns.

Sequence Diagram(s)

sequenceDiagram
    participant User as CLI User
    participant Run as run.ts (orchestrator)
    participant Cmd as Command (daily/monthly/session)
    participant Agg as DataAggregator
    participant Claude as Claude Loader
    participant Codex as Codex Loader
    participant OpenCode as OpenCode Loader
    participant Pi as Pi Loader
    participant Norm as Normalizers

    User->>Run: ccusage-omni daily --sources claude,codex --since=2026-01-01
    Run->>Cmd: execute command with args
    Cmd->>Agg: loadCombinedDailyData({sources,since,until,timezone,offline})
    Agg->>Claude: loadClaudeDaily(options)
    Claude-->>Agg: daily data
    Agg->>Codex: loadCodexDaily(options)
    Codex-->>Agg: daily data
    Agg->>OpenCode: loadOpenCodeDaily/options
    OpenCode-->>Agg: daily data
    Agg->>Pi: loadPiDaily/options
    Pi-->>Agg: daily data
    Agg->>Norm: normalize per-source -> UnifiedDailyUsage[]
    Norm-->>Agg: unified data
    Agg->>Agg: calculateTotals(data)
    Agg-->>Cmd: CombinedResult { data, totals }
    Cmd->>Cmd: render table/JSON (uses terminal table sizing)
    Cmd-->>User: rendered report
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • ryoppippi

Poem

🐰 Hopping through logs and timezone light,
I nibble duplicates and sort dates right,
Models lined in rows, costs cozy and neat,
Omni gathers data for a tasty treat,
Carrots, tables, CLI — what a delight! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main feature addition: a unified CLI (omni) that aggregates usage data across multiple AI coding assistant sources.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

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: 7

🤖 Fix all issues with AI agents
In `@apps/omni/src/_normalizers/codex.ts`:
- Around line 4-29: In normalizeCodexDaily and normalizeCodexMonthly, fix the
incorrect field and models handling: assign cacheReadTokens from
data.cacheReadTokens (not data.cachedInputTokens) and set models to data.models
directly (not Object.keys(data.models)); also guard the optional costUSD and
totalTokens fields by using null-coalescing/defaults (e.g., fallback to 0 or
null) when mapping data.costUSD and data.totalTokens so the returned
UnifiedDailyUsage/UnifiedMonthlyUsage never receives undefined.

In `@apps/omni/src/_normalizers/pi.ts`:
- Around line 59-69: The test fixture's modelsUsed array in the data object
(satisfies DailyUsageWithSource) uses an outdated model name '[pi]
claude-opus-4-5'; update that string to a current Claude 4 pricing name such as
'claude-opus-4-20250514' (or 'claude-sonnet-4-20250514') and ensure any related
entries in modelBreakdowns or other test fixtures match the same updated model
identifier so the test data aligns with current Claude 4 pricing names.

In `@apps/opencode/src/data-loader.ts`:
- Around line 365-371: The current date-filter branch uses Date.now() when
message.time.created is missing which can mistakenly include messages; update
the logic in the block that computes createdMs (the variables/identifiers:
createdMs, message.time.created, hasDateFilter, getDateKeyFromTimestamp,
isWithinRange, since, until) so that when hasDateFilter is true you do NOT fall
back to Date.now(): first prefer the file mtime value already available (use the
existing file mtime variable if present), and if message.time.created is missing
and no file mtime is available then skip this message (continue) instead of
using Date.now(); only call getDateKeyFromTimestamp and isWithinRange when you
have a real timestamp.
- Around line 130-142: normalizeDateInput currently returns undefined for
non-null/non-empty invalid date strings, silently disabling filters; change it
to surface errors by throwing a descriptive Error when a trimmed, non-empty
value does not match the expected YYYYMMDD/ YYYY-MM-DD pattern. Update the
function normalizeDateInput to validate the input (use the existing trimmed and
compact logic) and throw new Error(`Invalid date filter: "${value}"`) (or
similar) instead of returning undefined for invalid formats so callers can
catch/log and avoid assuming the filter was applied.

In `@OMNI_PLAN.md`:
- Around line 230-232: The fenced code block containing the directory tree
snippet starting with "apps/omni/" should include a language identifier (e.g.,
text) to satisfy markdownlint rule MD040; update the fenced block in
OMNI_PLAN.md from ``` to ```text so the directory tree (apps/omni/ ├── src/) is
marked as plain text.
- Around line 73-95: The JSON code blocks containing the "exports" and
"publishConfig" objects contain hard tab characters that trigger MD010; replace
all hard tabs with spaces in those fenced JSON snippets (the blocks showing
"exports": { ... } and "publishConfig": { ... }) so the markdown linter no
longer flags them, ensuring indentation uses spaces consistently throughout the
OMNI_PLAN.md examples.
- Around line 9-16: The markdown table under the "Supported Sources (v1):"
heading needs blank lines before and after it to satisfy MD058; edit the
markdown so there is an empty line between the heading and the table header row
and another empty line after the table (i.e., add a blank line above the line
starting with "| Source" and a blank line after the final table row) to resolve
the lint warning.
🧹 Nitpick comments (12)
apps/codex/src/daily-report.ts (1)

126-206: Consider adding a test case for formatDate: false.

The existing test validates the default formatting behavior, but adding a test case with formatDate: false would ensure the raw date key passthrough is covered.

💡 Example test addition
it('returns raw date keys when formatDate is false', async () => {
	const stubPricingSource: PricingSource = {
		async getPricing(): Promise<ModelPricing> {
			return { inputCostPerMToken: 1, cachedInputCostPerMToken: 0.1, outputCostPerMToken: 2 };
		},
	};
	const report = await buildDailyReport(
		[{
			sessionId: 's1',
			timestamp: '2025-09-11T03:00:00.000Z',
			model: 'gpt-5',
			inputTokens: 100,
			cachedInputTokens: 0,
			outputTokens: 50,
			reasoningOutputTokens: 0,
			totalTokens: 150,
		}],
		{ pricingSource: stubPricingSource, formatDate: false },
	);
	expect(report[0]!.date).toBe('2025-09-11');
});
apps/omni/src/run.ts (1)

16-29: Consider deriving the alternate name from the package name.

The hardcoded 'ccusage-omni' string duplicates knowledge that could potentially be derived. If the package name in package.json is @ccusage/omni, you might consider deriving the alternate form programmatically (e.g., name.replace('@ccusage/', 'ccusage-')).

That said, if this is intentional for handling a specific invocation pattern, the current approach is acceptable.

♻️ Optional: derive alternate name
 export async function run(): Promise<void> {
 	let args = process.argv.slice(2);
-	if (args[0] === name || args[0] === 'ccusage-omni') {
+	const alternateName = name.replace('@ccusage/', 'ccusage-');
+	if (args[0] === name || args[0] === alternateName) {
 		args = args.slice(1);
 	}
apps/omni/src/commands/session.ts (1)

83-100: Prefer Result.try() over try/catch for parse/normalize flow.
Aligns with the project’s functional error-handling guideline.

♻️ Suggested refactor
-import { log, logger } from '../logger.ts';
+import { log, logger } from '../logger.ts';
+import { Result } from '@praha/byethrow';

-		try {
-			sources = parseSources(ctx.values.sources);
-			since = normalizeDateInput(ctx.values.since);
-			until = normalizeDateInput(ctx.values.until);
-
-			if (ctx.values.days != null) {
-				const range = resolveDateRangeFromDays(ctx.values.days, ctx.values.timezone);
-				since = range.since;
-				until = range.until;
-			}
-		} catch (error) {
-			logger.error(String(error));
-			process.exit(1);
-		}
+		const parsed = Result.try(() => {
+			const parsedSources = parseSources(ctx.values.sources);
+			let parsedSince = normalizeDateInput(ctx.values.since);
+			let parsedUntil = normalizeDateInput(ctx.values.until);
+			if (ctx.values.days != null) {
+				const range = resolveDateRangeFromDays(ctx.values.days, ctx.values.timezone);
+				parsedSince = range.since;
+				parsedUntil = range.until;
+			}
+			return { parsedSources, parsedSince, parsedUntil };
+		});
+
+		if (Result.isFailure(parsed)) {
+			logger.error(String(parsed.error));
+			process.exit(1);
+		}
+
+		sources = parsed.value.parsedSources;
+		since = parsed.value.parsedSince;
+		until = parsed.value.parsedUntil;
packages/internal/src/pricing.ts (1)

91-117: Consider clearing shared caches when clearCache() is used.

With sharedOnlinePricing, per-instance clearCache() no longer forces a refresh. If refresh behavior is expected, consider evicting shared entries for this.url as well.

apps/ccusage/src/data-loader.ts (1)

719-737: Make date-key formatting explicit in filterFilesBySince.

Passing DEFAULT_LOCALE here keeps the date key stable even if the formatDate default ever changes.

🔧 Suggested fix
-					const dateKey = formatDate(new Date(fileStat.mtimeMs).toISOString(), timezone).replace(
-						/-/g,
-						'',
-					);
+					const dateKey = formatDate(
+						new Date(fileStat.mtimeMs).toISOString(),
+						timezone,
+						DEFAULT_LOCALE,
+					).replace(/-/g, '');
apps/codex/src/data-loader.ts (2)

192-206: Consider extracting shared date normalization logic.

This normalizeDateInput function duplicates logic from apps/omni/src/data-aggregator.ts (lines 560-571), though with different output formats (compact YYYYMMDD here vs YYYY-MM-DD there). The behavioral difference is intentional for dateKey comparisons, but consider extracting a shared helper that accepts a format parameter to reduce duplication.


244-257: File mtime filtering only checks since, not until.

The file-level prefiltering skips files with mtime < since but doesn't check until. This is acceptable as a performance optimization—files modified after the until date may still contain events within the range. However, add a brief comment explaining this asymmetry to clarify intent for future maintainers.

📝 Suggested comment
 		for (const file of files) {
+			// Optimization: Skip files modified before `since` as they cannot contain
+			// relevant events. We don't filter by `until` here since file mtime may
+			// be later than the events it contains.
 			if (since != null) {
 				try {
apps/omni/src/commands/monthly.ts (1)

88-92: Silent override when both days and since/until are provided.

When days is specified, it silently overrides any since/until values. Consider logging a warning to inform users that their explicit date range is being ignored.

📝 Suggested improvement
 			if (ctx.values.days != null) {
+				if (ctx.values.since != null || ctx.values.until != null) {
+					logger.warn('--days overrides --since/--until; ignoring explicit date range');
+				}
 				const range = resolveDateRangeFromDays(ctx.values.days, ctx.values.timezone);
 				since = range.since;
 				until = range.until;
 			}
apps/opencode/src/daily-report.ts (1)

37-44: Sequential await in loop may impact performance for large datasets.

Each calculateCostForEntry call is awaited sequentially. If the pricing fetcher involves network calls without caching, this could be slow. Consider batching cost calculations or using Promise.all if entries can be processed in parallel.

However, if pricingFetcher caches results internally (which is typical), this may be acceptable.

♻️ Alternative using Promise.all for parallel cost calculation
+	// Pre-calculate all costs in parallel
+	const costs = await Promise.all(
+		entries.map((entry) => calculateCostForEntry(entry, options.pricingFetcher)),
+	);
+	const costByEntry = new Map(entries.map((entry, i) => [entry, costs[i]!]));
+
 	for (const [date, dayEntries] of Object.entries(entriesByDate)) {
 		let inputTokens = 0;
 		let outputTokens = 0;
 		let cacheCreationTokens = 0;
 		let cacheReadTokens = 0;
 		let totalCost = 0;
 		const modelsSet = new Set<string>();

 		for (const entry of dayEntries) {
 			inputTokens += entry.usage.inputTokens;
 			outputTokens += entry.usage.outputTokens;
 			cacheCreationTokens += entry.usage.cacheCreationInputTokens;
 			cacheReadTokens += entry.usage.cacheReadInputTokens;
-			totalCost += await calculateCostForEntry(entry, options.pricingFetcher);
+			totalCost += costByEntry.get(entry) ?? 0;
 			modelsSet.add(entry.model);
 		}
apps/opencode/src/monthly-report.ts (1)

21-63: High code duplication with daily-report.ts.

The buildMonthlyReport function is nearly identical to buildDailyReport, differing only in the grouping key (slice(0, 7) vs split('T')[0]). Consider extracting a shared buildReport helper parameterized by a grouping function to reduce duplication.

♻️ Conceptual refactor to reduce duplication
// Shared helper in a common module
type ReportOptions<T> = {
  pricingFetcher: LiteLLMPricingFetcher;
  groupKey: (entry: LoadedUsageEntry) => string;
  keyName: 'date' | 'month';
};

async function buildReport<T extends { [K in keyof T]: T[K] }>(
  entries: LoadedUsageEntry[],
  options: ReportOptions<T>,
): Promise<T[]> {
  // Shared aggregation logic
}

// Then in daily-report.ts:
export const buildDailyReport = (entries, options) =>
  buildReport(entries, {
    ...options,
    groupKey: (e) => e.timestamp.toISOString().split('T')[0]!,
    keyName: 'date',
  });
apps/omni/src/commands/_shared.ts (1)

23-39: Consider handling empty bySource array.

If totals.bySource is empty (e.g., no data from any source), the output would show only the header and total line. While this may be acceptable, consider adding a check or a "No source data" message for clarity.

📝 Optional guard for empty bySource
 export function formatCostSummary(totals: CombinedTotals): string {
+	if (totals.bySource.length === 0) {
+		return `By Source (Cost)\n  TOTAL ........ ${formatCurrency(totals.costUSD)}`;
+	}
+
 	const labels = totals.bySource.map((entry) => SOURCE_LABELS[entry.source]);
apps/opencode/src/session-report.ts (1)

25-28: Add JSDoc for exported buildSessionReport.

Exported functions in this area should include brief JSDoc for purpose, parameters, and return value. As per coding guidelines, add a short doc block above the function.

Comment on lines +4 to +29
export function normalizeCodexDaily(data: DailyReportRow): UnifiedDailyUsage {
return {
source: 'codex',
date: data.date,
inputTokens: data.inputTokens,
outputTokens: data.outputTokens,
cacheReadTokens: data.cachedInputTokens,
cacheCreationTokens: 0,
totalTokens: data.totalTokens,
costUSD: data.costUSD,
models: Object.keys(data.models),
};
}

export function normalizeCodexMonthly(data: MonthlyReportRow): UnifiedMonthlyUsage {
return {
source: 'codex',
month: data.month,
inputTokens: data.inputTokens,
outputTokens: data.outputTokens,
cacheReadTokens: data.cachedInputTokens,
cacheCreationTokens: 0,
totalTokens: data.totalTokens,
costUSD: data.costUSD,
models: Object.keys(data.models),
};
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Search for Codex type definitions in node_modules and project files
find . -path ./node_modules -prune -o -name "*.ts" -type f -print | xargs grep -l "DailyReportRow\|MonthlyReportRow" 2>/dev/null | head -20

Repository: ryoppippi/ccusage

Length of output: 178


🏁 Script executed:

# Search more broadly for `@ccusage/codex` or codex types
fd -t f "\.ts$" | xargs grep -l "DailyReportRow\|MonthlyReportRow" 2>/dev/null | grep -v node_modules | head -20

Repository: ryoppippi/ccusage

Length of output: 178


🏁 Script executed:

# Check the actual normalizer file to see what fields are being accessed
cat -n apps/omni/src/_normalizers/codex.ts 2>/dev/null || echo "File not found"

Repository: ryoppippi/ccusage

Length of output: 178


🏁 Script executed:

# Search for type definitions or interfaces that might define these row types
fd -t f "\.ts$" -path "*/codex/*" | head -20

Repository: ryoppippi/ccusage

Length of output: 178


🏁 Script executed:

# Look for any types file in the codex-related packages
find . -name "*types*" -o -name "*type.ts" 2>/dev/null | grep -i codex | head -20

Repository: ryoppippi/ccusage

Length of output: 178


🌐 Web query:

@ccusage/codex DailyReportRow MonthlyReportRow types definition

💡 Result:

I couldn't find published TypeScript declarations, but the ccusage JSON output docs show the report row shapes — here are TypeScript interfaces inferred from those examples:

  • DailyReportRow
    interface DailyReportRow {
    date: string; // "YYYY-MM-DD"
    models: string[]; // model names used that day
    inputTokens: number;
    outputTokens: number;
    cacheCreationTokens?: number;
    cacheReadTokens?: number;
    totalTokens?: number;
    costUSD?: number;
    }

  • MonthlyReportRow
    interface MonthlyReportRow {
    month: string; // "YYYY-MM"
    models: string[]; // model names used that month
    inputTokens: number;
    outputTokens: number;
    cacheCreationTokens?: number;
    cacheReadTokens?: number;
    totalTokens?: number;
    costUSD?: number;
    }

Source: ccusage JSON output examples (daily & monthly). [1]

References

  1. ccusage — JSON Output (daily/monthly examples). [ccusage JSON Output]. (ccusage.ryoppippi.com)

Citations:


🌐 Web query:

ryoppippi/ccusage codex types DailyReportRow cachedInputTokens

💡 Result:

Short answer: In the @ccusage/codex codebase the daily-report row uses the Codex token field name cachedInputTokens (from cumulative events cached_input_tokens) and the JSON/TypeScript report rows expose it as cachedInputTokens / cacheReadTokens depending on report (JSON output uses cachedInputTokens / cache_read_tokens semantics; the CLI display may show "Cache Read" / cacheReadTokens). See the Codex report schema and repo docs. [1][2][3]

Sources:

  • ccusage JSON output / daily report docs (shows cache fields in daily JSON). [1]
  • ryoppippi/ccusage GitHub repository (apps/codex package and README). [2]
  • Codex docs describing token_count fields including cached_input_tokens and how daily rows are built. [3]

🌐 Web query:

@ccusage/codex npm package types github

💡 Result:

You can find the package and source here:

  • GitHub repo (source, including the codex app at apps/codex — entry point apps/codex/src/index.ts): ryoppippi/ccusage. [1]
  • npm / package metadata and docs for the scoped package @ccusage/codex (published CLI package). [2]

Notes:

  • The repo is TypeScript-based and the codex package lives under apps/codex (so typings are authored in the source). [1][2]

Sources:
[1] ryoppippi/ccusage (GitHub).
[2] @ccusage/codex package documentation / registry (ccusage / npm listings).


Fix field name and Object.keys() bug in Codex normalizers.

Per the official ccusage JSON output documentation, the Codex report row types use cacheReadTokens (not cachedInputTokens), and models is already a string[], not an object—so Object.keys(data.models) will return numeric indices ["0", "1", ...] instead of model names.

Change line assignments:

  • cacheReadTokens: data.cacheReadTokens (not data.cachedInputTokens)
  • models: data.models (not Object.keys(data.models))

Additionally, costUSD and totalTokens are optional fields in the interface; add null-coalescing guards or ensure upstream always populates them.

🤖 Prompt for AI Agents
In `@apps/omni/src/_normalizers/codex.ts` around lines 4 - 29, In
normalizeCodexDaily and normalizeCodexMonthly, fix the incorrect field and
models handling: assign cacheReadTokens from data.cacheReadTokens (not
data.cachedInputTokens) and set models to data.models directly (not
Object.keys(data.models)); also guard the optional costUSD and totalTokens
fields by using null-coalescing/defaults (e.g., fallback to 0 or null) when
mapping data.costUSD and data.totalTokens so the returned
UnifiedDailyUsage/UnifiedMonthlyUsage never receives undefined.

Comment thread apps/omni/src/_normalizers/pi.ts
Comment thread apps/opencode/src/data-loader.ts
Comment thread apps/opencode/src/data-loader.ts Outdated
Comment thread OMNI_PLAN.md Outdated
Comment on lines +9 to +16
**Supported Sources (v1):**
| Source | Package | Data Directory | Env Override |
|--------|---------|----------------|--------------|
| Claude Code | `ccusage` | `~/.claude/projects/` or `~/.config/claude/projects/` | `CLAUDE_CONFIG_DIR` |
| OpenAI Codex | `@ccusage/codex` | `~/.codex/sessions/` | `CODEX_HOME` |
| OpenCode | `@ccusage/opencode` | `~/.local/share/opencode/storage/message/` | `OPENCODE_DATA_DIR` |
| Pi-agent | `@ccusage/pi` | `~/.pi/agent/sessions/` | `PI_AGENT_DIR` |

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 | 🟡 Minor

Add blank lines around tables (MD058).

The “Supported Sources (v1)” table isn’t surrounded by blank lines, which trips markdownlint.

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

10-10: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)

🤖 Prompt for AI Agents
In `@OMNI_PLAN.md` around lines 9 - 16, The markdown table under the "Supported
Sources (v1):" heading needs blank lines before and after it to satisfy MD058;
edit the markdown so there is an empty line between the heading and the table
header row and another empty line after the table (i.e., add a blank line above
the line starting with "| Source" and a blank line after the final table row) to
resolve the lint warning.

Comment thread OMNI_PLAN.md Outdated
Comment on lines +73 to +95
```json
{
"exports": {
".": "./src/index.ts",
"./data-loader": "./src/data-loader.ts",
"./daily-report": "./src/daily-report.ts",
"./monthly-report": "./src/monthly-report.ts",
"./session-report": "./src/session-report.ts",
"./types": "./src/_types.ts",
"./package.json": "./package.json"
},
"publishConfig": {
"exports": {
".": "./dist/index.js",
"./data-loader": "./dist/data-loader.js",
"./daily-report": "./dist/daily-report.js",
"./monthly-report": "./dist/monthly-report.js",
"./session-report": "./dist/session-report.js",
"./types": "./dist/_types.js",
"./package.json": "./package.json"
}
}
}
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 | 🟡 Minor

Replace hard tabs in code snippets (MD010).

Markdownlint flags hard tabs in these JSON snippets; switch to spaces to keep lint clean.

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

75-75: Hard tabs
Column: 1

(MD010, no-hard-tabs)


76-76: Hard tabs
Column: 1

(MD010, no-hard-tabs)


77-77: Hard tabs
Column: 1

(MD010, no-hard-tabs)


78-78: Hard tabs
Column: 1

(MD010, no-hard-tabs)


79-79: Hard tabs
Column: 1

(MD010, no-hard-tabs)


80-80: Hard tabs
Column: 1

(MD010, no-hard-tabs)


81-81: Hard tabs
Column: 1

(MD010, no-hard-tabs)


82-82: Hard tabs
Column: 1

(MD010, no-hard-tabs)


83-83: Hard tabs
Column: 1

(MD010, no-hard-tabs)


84-84: Hard tabs
Column: 1

(MD010, no-hard-tabs)


85-85: Hard tabs
Column: 1

(MD010, no-hard-tabs)


86-86: Hard tabs
Column: 1

(MD010, no-hard-tabs)


87-87: Hard tabs
Column: 1

(MD010, no-hard-tabs)


88-88: Hard tabs
Column: 1

(MD010, no-hard-tabs)


89-89: Hard tabs
Column: 1

(MD010, no-hard-tabs)


90-90: Hard tabs
Column: 1

(MD010, no-hard-tabs)


91-91: Hard tabs
Column: 1

(MD010, no-hard-tabs)


92-92: Hard tabs
Column: 1

(MD010, no-hard-tabs)


93-93: Hard tabs
Column: 1

(MD010, no-hard-tabs)


94-94: Hard tabs
Column: 1

(MD010, no-hard-tabs)

🤖 Prompt for AI Agents
In `@OMNI_PLAN.md` around lines 73 - 95, The JSON code blocks containing the
"exports" and "publishConfig" objects contain hard tab characters that trigger
MD010; replace all hard tabs with spaces in those fenced JSON snippets (the
blocks showing "exports": { ... } and "publishConfig": { ... }) so the markdown
linter no longer flags them, ensuring indentation uses spaces consistently
throughout the OMNI_PLAN.md examples.

Comment thread OMNI_PLAN.md Outdated
Comment on lines +230 to +232
```
apps/omni/
├── src/
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 | 🟡 Minor

Add a language identifier to fenced blocks (MD040).

Use a language like text for the directory tree block to satisfy markdownlint.

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

230-230: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In `@OMNI_PLAN.md` around lines 230 - 232, The fenced code block containing the
directory tree snippet starting with "apps/omni/" should include a language
identifier (e.g., text) to satisfy markdownlint rule MD040; update the fenced
block in OMNI_PLAN.md from ``` to ```text so the directory tree (apps/omni/ ├──
src/) is marked as plain text.

- Add UnifiedModelBreakdown type and modelBreakdowns field to unified types
- Update Claude, Codex, Pi normalizers to include model breakdowns
- OpenCode returns empty breakdowns (source doesn't provide this data)
- Add -b/--breakdown flag to daily command with breakdown row rendering
- Codex breakdowns show tokens but cost=0 (source doesn't provide per-model cost)
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

🤖 Fix all issues with AI agents
In `@apps/omni/src/commands/daily.ts`:
- Around line 129-146: Replace the try/catch with the object-based Result.try
API from `@praha/byethrow`: wrap the parsing block in Result.try({ try: () => {
const sources = parseSources(ctx.values.sources); const since =
normalizeDateInput(ctx.values.since); const until =
normalizeDateInput(ctx.values.until); if (ctx.values.days != null) { const range
= resolveDateRangeFromDays(ctx.values.days, ctx.values.timezone); return {
sources, since: range.since, until: range.until }; } return { sources, since,
until }; }, catch: (error) => { logger.error(String(error)); process.exit(1); }
}), then extract and assign sources, since, until from the Result.try return
value (e.g., const { sources, since, until } = Result.try({...})). Ensure you
reference parseSources, normalizeDateInput, resolveDateRangeFromDays, Result.try
and logger in the change.
🧹 Nitpick comments (1)
apps/omni/src/_types.ts (1)

6-7: Use UPPER_SNAKE_CASE for exported constants.

Sources is a constant array; consider renaming to SOURCES and updating references to align with the convention.

♻️ Suggested change
-export const Sources = ['claude', 'codex', 'opencode', 'pi'] as const;
-export type Source = TupleToUnion<typeof Sources>;
+export const SOURCES = ['claude', 'codex', 'opencode', 'pi'] as const;
+export type Source = TupleToUnion<typeof SOURCES>;

As per coding guidelines, Use UPPER_SNAKE_CASE for constants (e.g., DEFAULT_CLAUDE_CODE_PATH).

Comment on lines +129 to +146
let sources;
let since: string | undefined;
let until: string | undefined;

try {
sources = parseSources(ctx.values.sources);
since = normalizeDateInput(ctx.values.since);
until = normalizeDateInput(ctx.values.until);

if (ctx.values.days != null) {
const range = resolveDateRangeFromDays(ctx.values.days, ctx.values.timezone);
since = range.since;
until = range.until;
}
} catch (error) {
logger.error(String(error));
process.exit(1);
}
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Step 1: Check if file exists and read the relevant section
if [ -f "apps/omni/src/commands/daily.ts" ]; then
  echo "=== File found: apps/omni/src/commands/daily.ts ==="
  wc -l "apps/omni/src/commands/daily.ts"
  echo ""
  echo "=== Lines 120-160 ==="
  sed -n '120,160p' "apps/omni/src/commands/daily.ts"
else
  echo "File not found: apps/omni/src/commands/daily.ts"
fi

Repository: ryoppippi/ccusage

Length of output: 1119


🏁 Script executed:

#!/bin/bash
# Step 2: Check for existing Result.try() patterns in the codebase
echo "=== Existing Result.try() patterns ==="
rg "Result\.try\(" -A 5 -B 2 | head -100

Repository: ryoppippi/ccusage

Length of output: 5816


🏁 Script executed:

#!/bin/bash
# Step 3: Check `@praha/byethrow` import patterns
echo "=== `@praha/byethrow` imports in codebase ==="
rg "from ['\"]@praha/byethrow" | head -20

Repository: ryoppippi/ccusage

Length of output: 1622


🏁 Script executed:

#!/bin/bash
# Step 4: Check current imports in the daily.ts file
echo "=== Current imports in daily.ts ==="
head -30 "apps/omni/src/commands/daily.ts"

Repository: ryoppippi/ccusage

Length of output: 929


🏁 Script executed:

#!/bin/bash
# Check all imports in apps/omni/src/commands/daily.ts
echo "=== All imports in apps/omni/src/commands/daily.ts ==="
grep -n "^import" "apps/omni/src/commands/daily.ts"

Repository: ryoppippi/ccusage

Length of output: 417


🏁 Script executed:

#!/bin/bash
# Check if Result.try() is used anywhere in apps/omni
echo "=== Result usage in apps/omni ==="
rg "Result\.(try|isFailure|isSuccess)" apps/omni/ | head -30

Repository: ryoppippi/ccusage

Length of output: 94


🏁 Script executed:

#!/bin/bash
# Get exact signature/pattern of Result.try() - check the `@praha/byethrow` package
echo "=== Searching for Result.try pattern with object syntax ==="
rg "Result\.try\(\{" -A 3 | head -50

Repository: ryoppippi/ccusage

Length of output: 2823


Refactor to use Result.try() pattern with correct API syntax.

The codebase uses @praha/byethrow with the object-based API Result.try({ try: ..., catch: ... }), not function syntax. Refactor this block to follow that convention:

♻️ Suggested refactor
 import { define } from 'gunshi';
+import { Result } from '@praha/byethrow';
 import pc from 'picocolors';
@@
-		let sources;
-		let since: string | undefined;
-		let until: string | undefined;
-
-		try {
-			sources = parseSources(ctx.values.sources);
-			since = normalizeDateInput(ctx.values.since);
-			until = normalizeDateInput(ctx.values.until);
-
-			if (ctx.values.days != null) {
-				const range = resolveDateRangeFromDays(ctx.values.days, ctx.values.timezone);
-				since = range.since;
-				until = range.until;
-			}
-		} catch (error) {
-			logger.error(String(error));
-			process.exit(1);
-		}
+		const parseResult = Result.try({
+			try: () => {
+				const sources = parseSources(ctx.values.sources);
+				let since = normalizeDateInput(ctx.values.since);
+				let until = normalizeDateInput(ctx.values.until);
+
+				if (ctx.values.days != null) {
+					const range = resolveDateRangeFromDays(ctx.values.days, ctx.values.timezone);
+					since = range.since;
+					until = range.until;
+				}
+
+				return { sources, since, until };
+			},
+			catch: (error) => error,
+		});
+
+		if (Result.isFailure(parseResult)) {
+			logger.error(String(parseResult.error));
+			process.exit(1);
+		}
+
+		const { sources, since, until } = parseResult.value;
🤖 Prompt for AI Agents
In `@apps/omni/src/commands/daily.ts` around lines 129 - 146, Replace the
try/catch with the object-based Result.try API from `@praha/byethrow`: wrap the
parsing block in Result.try({ try: () => { const sources =
parseSources(ctx.values.sources); const since =
normalizeDateInput(ctx.values.since); const until =
normalizeDateInput(ctx.values.until); if (ctx.values.days != null) { const range
= resolveDateRangeFromDays(ctx.values.days, ctx.values.timezone); return {
sources, since: range.since, until: range.until }; } return { sources, since,
until }; }, catch: (error) => { logger.error(String(error)); process.exit(1); }
}), then extract and assign sources, since, until from the Result.try return
value (e.g., const { sources, since, until } = Result.try({...})). Ensure you
reference parseSources, normalizeDateInput, resolveDateRangeFromDays, Result.try
and logger in the change.

- Extract shared formatModelNameShort and pushBreakdownRows to _shared.ts
- Add -b/--breakdown flag to monthly command
- Add -b/--breakdown flag to session command
- All three commands (daily, monthly, session) now support per-model breakdowns
When Codex has only one model for a day/month/session, assign the full
cost to that model's breakdown instead of showing $0.00.

Multi-model entries still show $0.00 since Codex doesn't track per-model costs.
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: 2

🤖 Fix all issues with AI agents
In `@apps/omni/src/commands/monthly.ts`:
- Around line 88-105: The current try/catch should be replaced with Result-based
handling using `@praha/byethrow` and add an explicit type for sources to avoid
implicit any: declare sources as e.g. string[] | undefined (or the correct
concrete type) for the variable `sources`, then call
`parseSources(ctx.values.sources)` and handle its Result (or cast its return
type) rather than leaving it untyped; replace direct throws from
`normalizeDateInput()` and `resolveDateRangeFromDays()` by calling their
Result-returning variants or wrapping them with `Result.try(() => ...)` from
`@praha/byethrow` and handle errors via `match`/`unwrapOrElse` to call
`logger.error(String(err))` and `process.exit(1)`; finally, after calling
`process.exit(1)` add an explicit `return;` to stop execution (use the same
`logger` and `ctx.values` symbols and the function names `parseSources`,
`normalizeDateInput`, and `resolveDateRangeFromDays` to locate the code).

In `@apps/omni/src/commands/session.ts`:
- Line 25: The TABLE_COLUMN_COUNT constant and pushBreakdownRows currently build
6 breakdown columns and pad to 8, which misaligns the compact table (5 columns);
modify pushBreakdownRows (and any code paths between lines ~160-182 that add
model breakdown rows) to detect compact mode and skip adding the model/spacer
breakdown rows so only the compact columns (input, output, cache, cost, etc.)
are produced (or build exactly 5 columns) instead of padding to
TABLE_COLUMN_COUNT; ensure TABLE_COLUMN_COUNT usage is conditional or the
function returns early for compact mode so cost and other columns remain
aligned.
🧹 Nitpick comments (1)
apps/omni/src/commands/session.ts (1)

96-109: Replace try-catch with @praha/byethrow Result-based handling for consistency with project patterns.

Lines 96-109 should use Result.try() to wrap the parsing and normalization operations, then check Result.isFailure() for error handling instead of the try-catch block. This aligns with the project's functional error-handling convention documented in apps/omni/CLAUDE.md.

The refactored pattern would wrap parseSources(), normalizeDateInput(), and resolveDateRangeFromDays() calls using Result.try(), then use early return (if (Result.isFailure(result))) on failure.

Comment on lines +88 to +105
let sources;
let since: string | undefined;
let until: string | undefined;

try {
sources = parseSources(ctx.values.sources);
since = normalizeDateInput(ctx.values.since);
until = normalizeDateInput(ctx.values.until);

if (ctx.values.days != null) {
const range = resolveDateRangeFromDays(ctx.values.days, ctx.values.timezone);
since = range.since;
until = range.until;
}
} catch (error) {
logger.error(String(error));
process.exit(1);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check tsconfig.json for noImplicitAny setting
find apps/omni -name 'tsconfig*.json' -type f | head -5

Repository: ryoppippi/ccusage

Length of output: 84


🏁 Script executed:

#!/bin/bash
# Check Result usage in apps/omni
rg -n "Result\.(try|isFailure|isSuccess)" apps/omni/src --type ts --type tsx

Repository: ryoppippi/ccusage

Length of output: 88


🏁 Script executed:

#!/bin/bash
# Check current imports in monthly.ts
head -20 apps/omni/src/commands/monthly.ts

Repository: ryoppippi/ccusage

Length of output: 545


🏁 Script executed:

#!/bin/bash
# Check if `@praha/byethrow` is imported elsewhere in apps/omni
rg -n "from '@praha/byethrow'" apps/omni/src --type ts --type tsx

Repository: ryoppippi/ccusage

Length of output: 88


🏁 Script executed:

#!/bin/bash
# Read tsconfig.json to check noImplicitAny
cat apps/omni/tsconfig.json

Repository: ryoppippi/ccusage

Length of output: 710


🏁 Script executed:

#!/bin/bash
# Check Result usage in apps/omni (correct file type)
rg "Result\.(try|isFailure|isSuccess)" apps/omni/src -t ts

Repository: ryoppippi/ccusage

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Check `@praha/byethrow` imports
rg "from '@praha/byethrow'" apps/omni/src -t ts

Repository: ryoppippi/ccusage

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Check how errors are currently handled in monthly.ts and related files
rg "try|catch|throw" apps/omni/src -t ts -A 2 -B 2 | head -60

Repository: ryoppippi/ccusage

Length of output: 3243


🏁 Script executed:

#!/bin/bash
# Check if `@praha/byethrow` is in dependencies/imports anywhere in apps/omni
rg "@praha/byethrow" apps/omni -t ts

Repository: ryoppippi/ccusage

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Check what the functions being called actually do
cat apps/omni/src/data-aggregator.ts | head -100

Repository: ryoppippi/ccusage

Length of output: 3141


🏁 Script executed:

#!/bin/bash
# Find where parseSources, normalizeDateInput, resolveDateRangeFromDays are defined
rg "export.*function (parseSources|normalizeDateInput|resolveDateRangeFromDays)" apps/omni/src -t ts -A 10

Repository: ryoppippi/ccusage

Length of output: 2050


🏁 Script executed:

#!/bin/bash
# Check if `@praha/byethrow` is in apps/omni package.json or root package.json
find . -name "package.json" -type f -path "*/apps/omni*" | head -1

Repository: ryoppippi/ccusage

Length of output: 85


🏁 Script executed:

#!/bin/bash
# Check root and apps/omni for `@praha/byethrow` dependency
rg "@praha/byethrow" package.json workspaces.yaml -t yaml -t json 2>/dev/null || echo "Searching in package files..."
grep -r "@praha/byethrow" . --include="package.json" 2>/dev/null | head -5

Repository: ryoppippi/ccusage

Length of output: 481


Switch error handling to Result-based flow and add explicit type for sources.

The functions normalizeDateInput() and resolveDateRangeFromDays() throw on invalid inputs, and sources currently lacks a type annotation. Under strict mode (strict: true), this creates an implicit-any risk. Adopting the @praha/byethrow Result pattern aligns with the codebase's error-handling guidelines and ensures proper type safety. Adding an explicit return; after process.exit(1) also prevents continuation if exit is mocked in tests.

♻️ Proposed refactor
+import { Result } from '@praha/byethrow';
 import { log, logger } from '../logger.ts';
@@
-		let sources;
+		let sources: ReturnType<typeof parseSources> | undefined;
 		let since: string | undefined;
 		let until: string | undefined;
 
-		try {
-			sources = parseSources(ctx.values.sources);
-			since = normalizeDateInput(ctx.values.since);
-			until = normalizeDateInput(ctx.values.until);
-
-			if (ctx.values.days != null) {
-				const range = resolveDateRangeFromDays(ctx.values.days, ctx.values.timezone);
-				since = range.since;
-				until = range.until;
-			}
-		} catch (error) {
-			logger.error(String(error));
-			process.exit(1);
-		}
+		const parsed = Result.try(() => {
+			const parsedSources = parseSources(ctx.values.sources);
+			let parsedSince = normalizeDateInput(ctx.values.since);
+			let parsedUntil = normalizeDateInput(ctx.values.until);
+
+			if (ctx.values.days != null) {
+				const range = resolveDateRangeFromDays(ctx.values.days, ctx.values.timezone);
+				parsedSince = range.since;
+				parsedUntil = range.until;
+			}
+
+			return { parsedSources, parsedSince, parsedUntil };
+		});
+
+		if (Result.isFailure(parsed)) {
+			logger.error(String(parsed.error));
+			process.exit(1);
+			return;
+		}
+
+		({ parsedSources: sources, parsedSince: since, parsedUntil: until } = parsed.value);
🤖 Prompt for AI Agents
In `@apps/omni/src/commands/monthly.ts` around lines 88 - 105, The current
try/catch should be replaced with Result-based handling using `@praha/byethrow`
and add an explicit type for sources to avoid implicit any: declare sources as
e.g. string[] | undefined (or the correct concrete type) for the variable
`sources`, then call `parseSources(ctx.values.sources)` and handle its Result
(or cast its return type) rather than leaving it untyped; replace direct throws
from `normalizeDateInput()` and `resolveDateRangeFromDays()` by calling their
Result-returning variants or wrapping them with `Result.try(() => ...)` from
`@praha/byethrow` and handle errors via `match`/`unwrapOrElse` to call
`logger.error(String(err))` and `process.exit(1)`; finally, after calling
`process.exit(1)` add an explicit `return;` to stop execution (use the same
`logger` and `ctx.values` symbols and the function names `parseSources`,
`normalizeDateInput`, and `resolveDateRangeFromDays` to locate the code).

pushBreakdownRows,
} from './_shared.ts';

const TABLE_COLUMN_COUNT = 8;
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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's explore the structure and find relevant files
find . -name "table.ts" -o -name "session.ts" | head -20

Repository: ryoppippi/ccusage

Length of output: 314


🏁 Script executed:

# Inspect the ResponsiveTable class and compact mode handling
rg -n "class ResponsiveTable" -A 10 --type ts

Repository: ryoppippi/ccusage

Length of output: 828


🏁 Script executed:

# Look for isCompactMode method/property
rg -n "isCompactMode" --type ts -B 2 -A 2

Repository: ryoppippi/ccusage

Length of output: 8391


🏁 Script executed:

# Check the session.ts file around line 25 and 160-182
cat -n apps/omni/src/commands/session.ts | head -200

Repository: ryoppippi/ccusage

Length of output: 5950


🏁 Script executed:

# Find the pushBreakdownRows function
rg -n "pushBreakdownRows" --type ts -B 2 -A 15

Repository: ryoppippi/ccusage

Length of output: 27509


🏁 Script executed:

# Verify the complete pushBreakdownRows implementation in omni
sed -n '71,95p' apps/omni/src/commands/_shared.ts

Repository: ryoppippi/ccusage

Length of output: 752


Skip model breakdown rows in compact mode to prevent column misalignment.

In compact mode, the table has 5 columns, but pushBreakdownRows builds 6 columns (model, spacer, input, output, cache, cost) and pads them to 8 (TABLE_COLUMN_COUNT). This shifts the cost column or causes truncation in the compact layout.

Fix
-			if (showBreakdown && row.modelBreakdowns.length > 0) {
+			if (showBreakdown && row.modelBreakdowns.length > 0 && !table.isCompactMode()) {
 				pushBreakdownRows(table, row.modelBreakdowns, TABLE_COLUMN_COUNT);
 			}

Also applies to: 160-182

🤖 Prompt for AI Agents
In `@apps/omni/src/commands/session.ts` at line 25, The TABLE_COLUMN_COUNT
constant and pushBreakdownRows currently build 6 breakdown columns and pad to 8,
which misaligns the compact table (5 columns); modify pushBreakdownRows (and any
code paths between lines ~160-182 that add model breakdown rows) to detect
compact mode and skip adding the model/spacer breakdown rows so only the compact
columns (input, output, cache, cost, etc.) are produced (or build exactly 5
columns) instead of padding to TABLE_COLUMN_COUNT; ensure TABLE_COLUMN_COUNT
usage is conditional or the function returns early for compact mode so cost and
other columns remain aligned.

Session table has 3 leading columns (Source, Session, Last Activity)
but breakdown rows only had 1 empty placeholder after the model name,
causing numeric values to shift left under the wrong headers.

Add leadingEmptyColumns parameter to pushBreakdownRows (default=1),
and pass 2 for session tables.
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: 2

🤖 Fix all issues with AI agents
In `@apps/codex/src/daily-report.ts`:
- Around line 208-213: Update the test to use the current Claude 4 model names
everywhere instead of gpt-5/gpt-5-mini: replace occurrences of
first.models['gpt-5'] and first.models['gpt-5-mini'] (and any pricing map keys,
event model entries, and variables like expectedGpt5Cost/expectedMiniCost) with
claude-opus-4-20250514 and claude-sonnet-4-20250514 respectively, and update the
corresponding expected cost calculations to reference those model keys so they
match LiteLLM's pricing database; apply the same replacement pattern in
apps/codex/src/session-report.ts and apps/codex/src/monthly-report.ts.

In `@apps/codex/src/session-report.ts`:
- Around line 238-243: Tests reference OpenAI models 'gpt-5' and 'gpt-5-mini' in
the pricing/assertion logic (see variables expectedGpt5Cost, expectedMiniCost
and checks on second.models['gpt-5'] and second.models['gpt-5-mini']) but must
use current Claude 4 entries; update all occurrences in the pricing map, any
event model entries, and the assertions so 'gpt-5' -> 'claude-opus-4-20250514'
and 'gpt-5-mini' -> 'claude-sonnet-4-20250514' (including the expected cost
variables and the expect(...) calls referencing second.models[...] ) to keep
names consistent across the test.

Comment on lines +208 to +213
const expectedGpt5Cost =
(800 / 1_000_000) * 1.25 + (200 / 1_000_000) * 0.125 + (500 / 1_000_000) * 10;
const expectedMiniCost =
(300 / 1_000_000) * 0.6 + (100 / 1_000_000) * 0.06 + (200 / 1_000_000) * 2;
expect(first.models['gpt-5']?.costUSD).toBeCloseTo(expectedGpt5Cost, 10);
expect(first.models['gpt-5-mini']?.costUSD).toBeCloseTo(expectedMiniCost, 10);
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

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "daily-report.ts" | head -20

Repository: ryoppippi/ccusage

Length of output: 129


🏁 Script executed:

fd -t f "daily-report.ts"

Repository: ryoppippi/ccusage

Length of output: 125


🏁 Script executed:

wc -l apps/codex/src/daily-report.ts

Repository: ryoppippi/ccusage

Length of output: 95


🏁 Script executed:

cat -n apps/codex/src/daily-report.ts | sed -n '180,230p'

Repository: ryoppippi/ccusage

Length of output: 1624


🏁 Script executed:

cat -n apps/codex/src/daily-report.ts | head -180

Repository: ryoppippi/ccusage

Length of output: 6123


🏁 Script executed:

rg "gpt-5" --type ts -n

Repository: ryoppippi/ccusage

Length of output: 3930


🏁 Script executed:

rg "claude-opus-4-20250514|claude-sonnet-4-20250514" --type ts -n | head -30

Repository: ryoppippi/ccusage

Length of output: 2492


🏁 Script executed:

cat -n apps/codex/src/session-report.ts | sed -n '150,250p'

Repository: ryoppippi/ccusage

Length of output: 3479


🏁 Script executed:

cat -n apps/codex/src/monthly-report.ts | sed -n '120,220p'

Repository: ryoppippi/ccusage

Length of output: 3589


Update test model names to current Claude 4 entries.

The per-model assertions and all test setup references still use gpt-5/gpt-5-mini. Please switch to claude-opus-4-20250514 and claude-sonnet-4-20250514 throughout the test (pricing map + event models + expectations).

As per coding guidelines: "All test files must use current Claude 4 models (claude-sonnet-4-20250514, claude-opus-4-20250514), not outdated Claude 3 models" and "Model names in tests must exactly match LiteLLM's pricing database entries."

✅ Proposed update
-					'gpt-5',
+					'claude-opus-4-20250514',
 					{ inputCostPerMToken: 1.25, cachedInputCostPerMToken: 0.125, outputCostPerMToken: 10 },
 				],
 				[
-					'gpt-5-mini',
+					'claude-sonnet-4-20250514',
 					{ inputCostPerMToken: 0.6, cachedInputCostPerMToken: 0.06, outputCostPerMToken: 2 },
 				],
 			]);
@@
-						model: 'gpt-5',
+						model: 'claude-opus-4-20250514',
@@
-						model: 'gpt-5-mini',
+						model: 'claude-sonnet-4-20250514',
@@
-						model: 'gpt-5',
+						model: 'claude-opus-4-20250514',
@@
-			const expectedGpt5Cost =
+			const expectedOpusCost =
 				(800 / 1_000_000) * 1.25 + (200 / 1_000_000) * 0.125 + (500 / 1_000_000) * 10;
-			const expectedMiniCost =
+			const expectedSonnetCost =
 				(300 / 1_000_000) * 0.6 + (100 / 1_000_000) * 0.06 + (200 / 1_000_000) * 2;
-			expect(first.models['gpt-5']?.costUSD).toBeCloseTo(expectedGpt5Cost, 10);
-			expect(first.models['gpt-5-mini']?.costUSD).toBeCloseTo(expectedMiniCost, 10);
+			expect(first.models['claude-opus-4-20250514']?.costUSD).toBeCloseTo(expectedOpusCost, 10);
+			expect(first.models['claude-sonnet-4-20250514']?.costUSD).toBeCloseTo(expectedSonnetCost, 10);

Note: Similar changes are needed in apps/codex/src/session-report.ts and apps/codex/src/monthly-report.ts which use the same test pattern with outdated model names.

🤖 Prompt for AI Agents
In `@apps/codex/src/daily-report.ts` around lines 208 - 213, Update the test to
use the current Claude 4 model names everywhere instead of gpt-5/gpt-5-mini:
replace occurrences of first.models['gpt-5'] and first.models['gpt-5-mini'] (and
any pricing map keys, event model entries, and variables like
expectedGpt5Cost/expectedMiniCost) with claude-opus-4-20250514 and
claude-sonnet-4-20250514 respectively, and update the corresponding expected
cost calculations to reference those model keys so they match LiteLLM's pricing
database; apply the same replacement pattern in apps/codex/src/session-report.ts
and apps/codex/src/monthly-report.ts.

Comment on lines +238 to +243
const expectedGpt5Cost =
(900 / 1_000_000) * 1.25 + (100 / 1_000_000) * 0.125 + (500 / 1_000_000) * 10;
const expectedMiniCost =
(300 / 1_000_000) * 0.6 + (100 / 1_000_000) * 0.06 + (200 / 1_000_000) * 2;
expect(second.models['gpt-5']?.costUSD).toBeCloseTo(expectedGpt5Cost, 10);
expect(second.models['gpt-5-mini']?.costUSD).toBeCloseTo(expectedMiniCost, 10);
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

🧩 Analysis chain

🏁 Script executed:

# First, let's see the actual content of the file around the specified lines
cat -n apps/codex/src/session-report.ts | sed -n '200,280p'

Repository: ryoppippi/ccusage

Length of output: 1869


🏁 Script executed:

# Also check what models are actually defined/used in this file
rg -i 'gpt-5|claude-opus|claude-sonnet' apps/codex/src/session-report.ts -B 2 -A 2

Repository: ryoppippi/ccusage

Length of output: 1280


🏁 Script executed:

# Check if there's any pricing or model configuration in the file
rg -i 'pricing|costPerMToken' apps/codex/src/session-report.ts -B 3 -A 3

Repository: ryoppippi/ccusage

Length of output: 1934


Update test model names to current Claude 4 entries.

The test uses gpt-5 and gpt-5-mini (OpenAI models), which violates the requirement that test files must use current Claude 4 models. Replace with claude-opus-4-20250514 and claude-sonnet-4-20250514 throughout: pricing map definition, event models, and assertions at lines 238-243.

Required changes
-					'gpt-5',
+					'claude-opus-4-20250514',
 					{ inputCostPerMToken: 1.25, cachedInputCostPerMToken: 0.125, outputCostPerMToken: 10 },
 				],
 				[
-					'gpt-5-mini',
+					'claude-sonnet-4-20250514',
 					{ inputCostPerMToken: 0.6, cachedInputCostPerMToken: 0.06, outputCostPerMToken: 2 },
 				],
 			]);
@@
-						model: 'gpt-5',
+						model: 'claude-opus-4-20250514',
@@
-						model: 'gpt-5-mini',
+						model: 'claude-sonnet-4-20250514',
@@
-						model: 'gpt-5',
+						model: 'claude-opus-4-20250514',
@@
-			const expectedGpt5Cost =
+			const expectedOpusCost =
 				(900 / 1_000_000) * 1.25 + (100 / 1_000_000) * 0.125 + (500 / 1_000_000) * 10;
-			const expectedMiniCost =
+			const expectedSonnetCost =
 				(300 / 1_000_000) * 0.6 + (100 / 1_000_000) * 0.06 + (200 / 1_000_000) * 2;
-			expect(second.models['gpt-5']?.costUSD).toBeCloseTo(expectedGpt5Cost, 10);
-			expect(second.models['gpt-5-mini']?.costUSD).toBeCloseTo(expectedMiniCost, 10);
+			expect(second.models['claude-opus-4-20250514']?.costUSD).toBeCloseTo(expectedOpusCost, 10);
+			expect(second.models['claude-sonnet-4-20250514']?.costUSD).toBeCloseTo(expectedSonnetCost, 10);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const expectedGpt5Cost =
(900 / 1_000_000) * 1.25 + (100 / 1_000_000) * 0.125 + (500 / 1_000_000) * 10;
const expectedMiniCost =
(300 / 1_000_000) * 0.6 + (100 / 1_000_000) * 0.06 + (200 / 1_000_000) * 2;
expect(second.models['gpt-5']?.costUSD).toBeCloseTo(expectedGpt5Cost, 10);
expect(second.models['gpt-5-mini']?.costUSD).toBeCloseTo(expectedMiniCost, 10);
const expectedOpusCost =
(900 / 1_000_000) * 1.25 + (100 / 1_000_000) * 0.125 + (500 / 1_000_000) * 10;
const expectedSonnetCost =
(300 / 1_000_000) * 0.6 + (100 / 1_000_000) * 0.06 + (200 / 1_000_000) * 2;
expect(second.models['claude-opus-4-20250514']?.costUSD).toBeCloseTo(expectedOpusCost, 10);
expect(second.models['claude-sonnet-4-20250514']?.costUSD).toBeCloseTo(expectedSonnetCost, 10);
🤖 Prompt for AI Agents
In `@apps/codex/src/session-report.ts` around lines 238 - 243, Tests reference
OpenAI models 'gpt-5' and 'gpt-5-mini' in the pricing/assertion logic (see
variables expectedGpt5Cost, expectedMiniCost and checks on
second.models['gpt-5'] and second.models['gpt-5-mini']) but must use current
Claude 4 entries; update all occurrences in the pricing map, any event model
entries, and the assertions so 'gpt-5' -> 'claude-opus-4-20250514' and
'gpt-5-mini' -> 'claude-sonnet-4-20250514' (including the expected cost
variables and the expect(...) calls referencing second.models[...] ) to keep
names consistent across the test.

@ben-vargas
Copy link
Copy Markdown
Contributor Author

Closing since never looked at and my updates to my local version I use is causing me noise from the review bots.

@ben-vargas ben-vargas closed this Feb 2, 2026
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.

1 participant