feat(omni): add unified usage aggregation across AI coding assistants#813
feat(omni): add unified usage aggregation across AI coding assistants#813ben-vargas wants to merge 17 commits intoryoppippi:mainfrom
Conversation
📝 WalkthroughWalkthroughAdds 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
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 forformatDate: false.The existing test validates the default formatting behavior, but adding a test case with
formatDate: falsewould 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 inpackage.jsonis@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: PreferResult.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 whenclearCache()is used.With
sharedOnlinePricing, per-instanceclearCache()no longer forces a refresh. If refresh behavior is expected, consider evicting shared entries forthis.urlas well.apps/ccusage/src/data-loader.ts (1)
719-737: Make date-key formatting explicit infilterFilesBySince.Passing
DEFAULT_LOCALEhere 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
normalizeDateInputfunction duplicates logic fromapps/omni/src/data-aggregator.ts(lines 560-571), though with different output formats (compactYYYYMMDDhere vsYYYY-MM-DDthere). 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 checkssince, notuntil.The file-level prefiltering skips files with
mtime < sincebut doesn't checkuntil. This is acceptable as a performance optimization—files modified after theuntildate 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 bothdaysandsince/untilare provided.When
daysis specified, it silently overrides anysince/untilvalues. 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: Sequentialawaitin loop may impact performance for large datasets.Each
calculateCostForEntrycall is awaited sequentially. If the pricing fetcher involves network calls without caching, this could be slow. Consider batching cost calculations or usingPromise.allif entries can be processed in parallel.However, if
pricingFetchercaches 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 withdaily-report.ts.The
buildMonthlyReportfunction is nearly identical tobuildDailyReport, differing only in the grouping key (slice(0, 7)vssplit('T')[0]). Consider extracting a sharedbuildReporthelper 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 emptybySourcearray.If
totals.bySourceis 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 exportedbuildSessionReport.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.
| 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), | ||
| }; |
There was a problem hiding this comment.
🧩 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 -20Repository: 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 -20Repository: 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 -20Repository: 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 -20Repository: 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
- 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(notdata.cachedInputTokens)models: data.models(notObject.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.
| **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` | | ||
|
|
There was a problem hiding this comment.
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.
| ```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" | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| ``` | ||
| apps/omni/ | ||
| ├── src/ |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
Sourcesis a constant array; consider renaming toSOURCESand 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).
| 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); | ||
| } |
There was a problem hiding this comment.
🧩 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"
fiRepository: 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 -100Repository: 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 -20Repository: 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 -30Repository: 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 -50Repository: 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.
There was a problem hiding this comment.
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/byethrowResult-based handling for consistency with project patterns.Lines 96-109 should use
Result.try()to wrap the parsing and normalization operations, then checkResult.isFailure()for error handling instead of the try-catch block. This aligns with the project's functional error-handling convention documented inapps/omni/CLAUDE.md.The refactored pattern would wrap
parseSources(),normalizeDateInput(), andresolveDateRangeFromDays()calls usingResult.try(), then use early return (if (Result.isFailure(result))) on failure.
| 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); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check tsconfig.json for noImplicitAny setting
find apps/omni -name 'tsconfig*.json' -type f | head -5Repository: 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 tsxRepository: ryoppippi/ccusage
Length of output: 88
🏁 Script executed:
#!/bin/bash
# Check current imports in monthly.ts
head -20 apps/omni/src/commands/monthly.tsRepository: 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 tsxRepository: ryoppippi/ccusage
Length of output: 88
🏁 Script executed:
#!/bin/bash
# Read tsconfig.json to check noImplicitAny
cat apps/omni/tsconfig.jsonRepository: 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 tsRepository: ryoppippi/ccusage
Length of output: 43
🏁 Script executed:
#!/bin/bash
# Check `@praha/byethrow` imports
rg "from '@praha/byethrow'" apps/omni/src -t tsRepository: 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 -60Repository: 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 tsRepository: 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 -100Repository: 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 10Repository: 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 -1Repository: 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 -5Repository: 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; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's explore the structure and find relevant files
find . -name "table.ts" -o -name "session.ts" | head -20Repository: ryoppippi/ccusage
Length of output: 314
🏁 Script executed:
# Inspect the ResponsiveTable class and compact mode handling
rg -n "class ResponsiveTable" -A 10 --type tsRepository: ryoppippi/ccusage
Length of output: 828
🏁 Script executed:
# Look for isCompactMode method/property
rg -n "isCompactMode" --type ts -B 2 -A 2Repository: 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 -200Repository: ryoppippi/ccusage
Length of output: 5950
🏁 Script executed:
# Find the pushBreakdownRows function
rg -n "pushBreakdownRows" --type ts -B 2 -A 15Repository: ryoppippi/ccusage
Length of output: 27509
🏁 Script executed:
# Verify the complete pushBreakdownRows implementation in omni
sed -n '71,95p' apps/omni/src/commands/_shared.tsRepository: 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.
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "daily-report.ts" | head -20Repository: 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.tsRepository: 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 -180Repository: ryoppippi/ccusage
Length of output: 6123
🏁 Script executed:
rg "gpt-5" --type ts -nRepository: ryoppippi/ccusage
Length of output: 3930
🏁 Script executed:
rg "claude-opus-4-20250514|claude-sonnet-4-20250514" --type ts -n | head -30Repository: 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.
| 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); |
There was a problem hiding this comment.
🧩 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 2Repository: 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 3Repository: 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.
| 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.
|
Closing since never looked at and my updates to my local version I use is causing me noise from the review bots. |
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.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/omnisrc/_normalizers/- Per-source data normalizers (claude, codex, opencode, pi)src/data-aggregator.ts- Main aggregation logic with parallel loadingsrc/commands/- CLI commands (daily, monthly, session)Required Changes to Existing Packages
daily-report,monthly-report,session-report,data-loaderdata-loaderPerformance Optimizations
DateTimeFormatinstances in Codex for faster date/month key generationLiteLLMPricingFetcherso parallel source loaders share a single network requestUsage
Notes
Summary by CodeRabbit
New Features
Improvements
Documentation