Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions apps/codex/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ npx @ccusage/codex@latest daily
# Date range filtering
npx @ccusage/codex@latest daily --since 20250911 --until 20250917

# Multiple Codex homes (multi-account aggregation)
npx @ccusage/codex@latest monthly --codex-home ~/.codex-work,~/.codex-personal

# Multiple Codex homes with explicit account labels + per-account rows
npx @ccusage/codex@latest monthly --codex-home work=~/.codex-work,personal=~/.codex-personal --by-account

# JSON output for scripting
npx @ccusage/codex@latest daily --json

Expand All @@ -75,8 +81,8 @@ npx @ccusage/codex@latest sessions

Useful environment variables:

- `CODEX_HOME` – override the root directory that contains Codex session folders
- `LOG_LEVEL` – controla consola log verbosity (0 silent … 5 trace)
- `CODEX_HOME` – override Codex home(s). Supports single path or comma-separated list for multi-account aggregation (e.g. `work=~/.codex-work,personal=~/.codex-personal`)
- `LOG_LEVEL` – controls console log verbosity (0 silent … 5 trace)

ℹ️ The CLI now relies on the model metadata recorded in each `turn_context`. Sessions emitted during early September 2025 that lack this metadata are skipped to avoid mispricing. Newer builds of the Codex CLI restore the model field, and aliases such as `gpt-5-codex` automatically resolve to the correct LiteLLM pricing entry.
📦 For legacy JSONL files that never emitted `turn_context` metadata, the CLI falls back to treating the tokens as `gpt-5` so that usage still appears in reports (pricing is therefore approximate for those sessions). In JSON output you will also see `"isFallback": true` on those model entries.
Expand Down
10 changes: 10 additions & 0 deletions apps/codex/src/_shared-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ export const sharedArgs = {
short: 'u',
description: 'Filter until date (inclusive)',
},
codexHome: {
type: 'string',
description:
'Codex home path(s). Accept comma-separated values and optional label=path entries for multi-account usage',
},
byAccount: {
type: 'boolean',
description: 'Group report rows by account when multiple Codex homes are configured',
default: false,
},
timezone: {
type: 'string',
short: 'z',
Expand Down
12 changes: 12 additions & 0 deletions apps/codex/src/_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@ export type TokenUsageDelta = {
totalTokens: number;
};

export type SessionSource = {
account: string;
directory: string;
};

export type TokenUsageEvent = TokenUsageDelta & {
timestamp: string;
sessionId: string;
account?: string;
model?: string;
isFallbackModel?: boolean;
};
Expand All @@ -19,20 +25,23 @@ export type ModelUsage = TokenUsageDelta & {

export type DailyUsageSummary = {
date: string;
account?: string;
firstTimestamp: string;
costUSD: number;
models: Map<string, ModelUsage>;
} & TokenUsageDelta;

export type MonthlyUsageSummary = {
month: string;
account?: string;
firstTimestamp: string;
costUSD: number;
models: Map<string, ModelUsage>;
} & TokenUsageDelta;

export type SessionUsageSummary = {
sessionId: string;
account?: string;
firstTimestamp: string;
lastTimestamp: string;
costUSD: number;
Expand All @@ -56,6 +65,7 @@ export type PricingSource = {

export type DailyReportRow = {
date: string;
account?: string;
inputTokens: number;
cachedInputTokens: number;
outputTokens: number;
Expand All @@ -67,6 +77,7 @@ export type DailyReportRow = {

export type MonthlyReportRow = {
month: string;
account?: string;
inputTokens: number;
cachedInputTokens: number;
outputTokens: number;
Expand All @@ -78,6 +89,7 @@ export type MonthlyReportRow = {

export type SessionReportRow = {
sessionId: string;
account?: string;
lastActivity: string;
sessionFile: string;
directory: string;
Expand Down
109 changes: 67 additions & 42 deletions apps/codex/src/commands/daily.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ import process from 'node:process';
import {
addEmptySeparatorRow,
formatCurrency,
formatDateCompact,
formatModelsDisplayMultiline,
formatNumber,
ResponsiveTable,
} from '@ccusage/terminal/table';
import { define } from 'gunshi';
import pc from 'picocolors';
Expand All @@ -17,8 +15,8 @@ import { loadTokenUsageEvents } from '../data-loader.ts';
import { normalizeFilterDate } from '../date-utils.ts';
import { log, logger } from '../logger.ts';
import { CodexPricingSource } from '../pricing.ts';

const TABLE_COLUMN_COUNT = 8;
import { resolveSessionSources } from '../session-sources.ts';
import { createUsageResponsiveTable } from './usage-table.ts';

export const dailyCommand = define({
name: 'daily',
Expand All @@ -41,7 +39,12 @@ export const dailyCommand = define({
process.exit(1);
}

const { events, missingDirectories } = await loadTokenUsageEvents();
const sessionSources = resolveSessionSources(ctx.values.codexHome);
const byAccount = ctx.values.byAccount === true;
const hasMultipleAccounts = sessionSources.length > 1;
const { events, missingDirectories } = await loadTokenUsageEvents({
sessionSources,
});

for (const missing of missingDirectories) {
logger.warn(`Codex session directory not found: ${missing}`);
Expand All @@ -62,6 +65,7 @@ export const dailyCommand = define({
locale: ctx.values.locale,
since,
until,
byAccount,
});

if (rows.length === 0) {
Expand Down Expand Up @@ -111,24 +115,17 @@ export const dailyCommand = define({
`Codex Token Usage Report - Daily (Timezone: ${ctx.values.timezone ?? DEFAULT_TIMEZONE})`,
);

const table: ResponsiveTable = new ResponsiveTable({
head: [
'Date',
'Models',
'Input',
'Output',
'Reasoning',
'Cache Read',
'Total Tokens',
'Cost (USD)',
],
colAligns: ['left', 'left', 'right', 'right', 'right', 'right', 'right', 'right'],
compactHead: ['Date', 'Models', 'Input', 'Output', 'Cost (USD)'],
compactColAligns: ['left', 'left', 'right', 'right', 'right'],
compactThreshold: 100,
if (hasMultipleAccounts && !byAccount) {
logger.info(
'Aggregating usage across multiple accounts. Use --by-account to split rows by account.',
);
}

const includeAccountColumn = byAccount;
const { table, tableColumnCount } = createUsageResponsiveTable({
mode: 'daily',
includeAccountColumn,
forceCompact: ctx.values.compact,
style: { head: ['cyan'] },
dateFormatter: (dateStr: string) => formatDateCompact(dateStr),
});

const totalsForDisplay = {
Expand All @@ -149,30 +146,58 @@ export const dailyCommand = define({
totalsForDisplay.totalTokens += row.totalTokens;
totalsForDisplay.costUSD += row.costUSD;

if (includeAccountColumn) {
table.push([
row.date,
row.account ?? 'default',
formatModelsDisplayMultiline(formatModelsList(row.models)),
formatNumber(split.inputTokens),
formatNumber(split.outputTokens),
formatNumber(split.reasoningTokens),
formatNumber(split.cacheReadTokens),
formatNumber(row.totalTokens),
formatCurrency(row.costUSD),
]);
} else {
table.push([
row.date,
formatModelsDisplayMultiline(formatModelsList(row.models)),
formatNumber(split.inputTokens),
formatNumber(split.outputTokens),
formatNumber(split.reasoningTokens),
formatNumber(split.cacheReadTokens),
formatNumber(row.totalTokens),
formatCurrency(row.costUSD),
]);
}
}

addEmptySeparatorRow(table, tableColumnCount);
if (includeAccountColumn) {
table.push([
row.date,
formatModelsDisplayMultiline(formatModelsList(row.models)),
formatNumber(split.inputTokens),
formatNumber(split.outputTokens),
formatNumber(split.reasoningTokens),
formatNumber(split.cacheReadTokens),
formatNumber(row.totalTokens),
formatCurrency(row.costUSD),
pc.yellow('Total'),
'',
'',
pc.yellow(formatNumber(totalsForDisplay.inputTokens)),
pc.yellow(formatNumber(totalsForDisplay.outputTokens)),
pc.yellow(formatNumber(totalsForDisplay.reasoningTokens)),
pc.yellow(formatNumber(totalsForDisplay.cacheReadTokens)),
pc.yellow(formatNumber(totalsForDisplay.totalTokens)),
pc.yellow(formatCurrency(totalsForDisplay.costUSD)),
]);
} else {
table.push([
pc.yellow('Total'),
'',
pc.yellow(formatNumber(totalsForDisplay.inputTokens)),
pc.yellow(formatNumber(totalsForDisplay.outputTokens)),
pc.yellow(formatNumber(totalsForDisplay.reasoningTokens)),
pc.yellow(formatNumber(totalsForDisplay.cacheReadTokens)),
pc.yellow(formatNumber(totalsForDisplay.totalTokens)),
pc.yellow(formatCurrency(totalsForDisplay.costUSD)),
]);
}

addEmptySeparatorRow(table, TABLE_COLUMN_COUNT);
table.push([
pc.yellow('Total'),
'',
pc.yellow(formatNumber(totalsForDisplay.inputTokens)),
pc.yellow(formatNumber(totalsForDisplay.outputTokens)),
pc.yellow(formatNumber(totalsForDisplay.reasoningTokens)),
pc.yellow(formatNumber(totalsForDisplay.cacheReadTokens)),
pc.yellow(formatNumber(totalsForDisplay.totalTokens)),
pc.yellow(formatCurrency(totalsForDisplay.costUSD)),
]);

log(table.toString());

if (table.isCompactMode()) {
Expand Down
Loading