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
21 changes: 15 additions & 6 deletions apps/codex/src/commands/daily.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { DailyUsageSummary } from '../_types.ts';
import process from 'node:process';
import {
addEmptySeparatorRow,
Expand All @@ -12,7 +13,7 @@ import pc from 'picocolors';
import { DEFAULT_TIMEZONE } from '../_consts.ts';
import { sharedArgs } from '../_shared-args.ts';
import { formatModelsList, splitUsageTokens } from '../command-utils.ts';
import { buildDailyReport } from '../daily-report.ts';
import { accumulateDailyUsage, buildDailyReportRows } from '../daily-report.ts';
import { loadTokenUsageEvents } from '../data-loader.ts';
import { normalizeFilterDate } from '../date-utils.ts';
import { log, logger } from '../logger.ts';
Expand Down Expand Up @@ -41,13 +42,23 @@ export const dailyCommand = define({
process.exit(1);
}

const { events, missingDirectories } = await loadTokenUsageEvents();
const summaries = new Map<string, DailyUsageSummary>();
const { missingDirectories } = await loadTokenUsageEvents({
since,
until,
timezone: ctx.values.timezone,
collectEvents: false,
sortEvents: false,
onEvent: (event) => {
accumulateDailyUsage(summaries, event, ctx.values.timezone);
},
});

for (const missing of missingDirectories) {
logger.warn(`Codex session directory not found: ${missing}`);
}

if (events.length === 0) {
if (summaries.size === 0) {
log(jsonOutput ? JSON.stringify({ daily: [], totals: null }) : 'No Codex usage data found.');
return;
}
Expand All @@ -56,12 +67,10 @@ export const dailyCommand = define({
offline: ctx.values.offline,
});
try {
const rows = await buildDailyReport(events, {
const rows = await buildDailyReportRows(summaries, {
pricingSource,
timezone: ctx.values.timezone,
locale: ctx.values.locale,
since,
until,
});

if (rows.length === 0) {
Expand Down
21 changes: 15 additions & 6 deletions apps/codex/src/commands/monthly.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { MonthlyUsageSummary } from '../_types.ts';
import process from 'node:process';
import {
addEmptySeparatorRow,
Expand All @@ -15,7 +16,7 @@ import { formatModelsList, splitUsageTokens } from '../command-utils.ts';
import { loadTokenUsageEvents } from '../data-loader.ts';
import { normalizeFilterDate } from '../date-utils.ts';
import { log, logger } from '../logger.ts';
import { buildMonthlyReport } from '../monthly-report.ts';
import { accumulateMonthlyUsage, buildMonthlyReportRows } from '../monthly-report.ts';
import { CodexPricingSource } from '../pricing.ts';

const TABLE_COLUMN_COUNT = 8;
Expand All @@ -41,13 +42,23 @@ export const monthlyCommand = define({
process.exit(1);
}

const { events, missingDirectories } = await loadTokenUsageEvents();
const summaries = new Map<string, MonthlyUsageSummary>();
const { missingDirectories } = await loadTokenUsageEvents({
since,
until,
timezone: ctx.values.timezone,
collectEvents: false,
sortEvents: false,
onEvent: (event) => {
accumulateMonthlyUsage(summaries, event, ctx.values.timezone);
},
});

for (const missing of missingDirectories) {
logger.warn(`Codex session directory not found: ${missing}`);
}

if (events.length === 0) {
if (summaries.size === 0) {
log(
jsonOutput ? JSON.stringify({ monthly: [], totals: null }) : 'No Codex usage data found.',
);
Expand All @@ -58,12 +69,10 @@ export const monthlyCommand = define({
offline: ctx.values.offline,
});
try {
const rows = await buildMonthlyReport(events, {
const rows = await buildMonthlyReportRows(summaries, {
pricingSource,
timezone: ctx.values.timezone,
locale: ctx.values.locale,
since,
until,
});

if (rows.length === 0) {
Expand Down
25 changes: 15 additions & 10 deletions apps/codex/src/commands/session.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { SessionUsageSummary } from '../_types.ts';
import process from 'node:process';
import {
addEmptySeparatorRow,
Expand All @@ -21,7 +22,7 @@ import {
} from '../date-utils.ts';
import { log, logger } from '../logger.ts';
import { CodexPricingSource } from '../pricing.ts';
import { buildSessionReport } from '../session-report.ts';
import { accumulateSessionUsage, buildSessionReportRows } from '../session-report.ts';

const TABLE_COLUMN_COUNT = 11;

Expand All @@ -46,13 +47,23 @@ export const sessionCommand = define({
process.exit(1);
}

const { events, missingDirectories } = await loadTokenUsageEvents();
const summaries = new Map<string, SessionUsageSummary>();
const { missingDirectories } = await loadTokenUsageEvents({
since,
until,
timezone: ctx.values.timezone,
collectEvents: false,
sortEvents: false,
onEvent: (event) => {
accumulateSessionUsage(summaries, event);
},
});

for (const missing of missingDirectories) {
logger.warn(`Codex session directory not found: ${missing}`);
}

if (events.length === 0) {
if (summaries.size === 0) {
log(
jsonOutput ? JSON.stringify({ sessions: [], totals: null }) : 'No Codex usage data found.',
);
Expand All @@ -63,13 +74,7 @@ export const sessionCommand = define({
offline: ctx.values.offline,
});
try {
const rows = await buildSessionReport(events, {
pricingSource,
timezone: ctx.values.timezone,
locale: ctx.values.locale,
since,
until,
});
const rows = await buildSessionReportRows(summaries, { pricingSource });

if (rows.length === 0) {
log(
Expand Down
95 changes: 58 additions & 37 deletions apps/codex/src/daily-report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export type DailyReportOptions = {
pricingSource: PricingSource;
};

export type DailySummaries = Map<string, DailyUsageSummary>;

function createSummary(date: string, initialTimestamp: string): DailyUsageSummary {
return {
date,
Expand All @@ -31,46 +33,46 @@ function createSummary(date: string, initialTimestamp: string): DailyUsageSummar
};
}

export async function buildDailyReport(
events: TokenUsageEvent[],
options: DailyReportOptions,
): Promise<DailyReportRow[]> {
const timezone = options.timezone;
const locale = options.locale;
const since = options.since;
const until = options.until;
const pricingSource = options.pricingSource;

const summaries = new Map<string, DailyUsageSummary>();
export function accumulateDailyUsage(
summaries: DailySummaries,
event: TokenUsageEvent,
timezone?: string,
): void {
const modelName = event.model?.trim();
if (modelName == null || modelName === '') {
return;
}

for (const event of events) {
const modelName = event.model?.trim();
if (modelName == null || modelName === '') {
continue;
}
const dateKey = toDateKey(event.timestamp, timezone);
const summary = summaries.get(dateKey) ?? createSummary(dateKey, event.timestamp);
if (!summaries.has(dateKey)) {
summaries.set(dateKey, summary);
}

const dateKey = toDateKey(event.timestamp, timezone);
if (!isWithinRange(dateKey, since, until)) {
continue;
}
addUsage(summary, event);
const modelUsage: ModelUsage = summary.models.get(modelName) ?? {
...createEmptyUsage(),
isFallback: false,
};
if (!summary.models.has(modelName)) {
summary.models.set(modelName, modelUsage);
}
addUsage(modelUsage, event);
if (event.isFallbackModel === true) {
modelUsage.isFallback = true;
}
}

const summary = summaries.get(dateKey) ?? createSummary(dateKey, event.timestamp);
if (!summaries.has(dateKey)) {
summaries.set(dateKey, summary);
}
export async function buildDailyReportRows(
summaries: DailySummaries,
options: Omit<DailyReportOptions, 'since' | 'until'>,
): Promise<DailyReportRow[]> {
const locale = options.locale;
const timezone = options.timezone;
const pricingSource = options.pricingSource;

addUsage(summary, event);
const modelUsage: ModelUsage = summary.models.get(modelName) ?? {
...createEmptyUsage(),
isFallback: false,
};
if (!summary.models.has(modelName)) {
summary.models.set(modelName, modelUsage);
}
addUsage(modelUsage, event);
if (event.isFallbackModel === true) {
modelUsage.isFallback = true;
}
if (summaries.size === 0) {
return [];
}

const uniqueModels = new Set<string>();
Expand All @@ -86,7 +88,6 @@ export async function buildDailyReport(
}

const rows: DailyReportRow[] = [];

const sortedSummaries = Array.from(summaries.values()).sort((a, b) =>
a.date.localeCompare(b.date),
);
Expand Down Expand Up @@ -121,6 +122,26 @@ export async function buildDailyReport(
return rows;
}

export async function buildDailyReport(
events: TokenUsageEvent[],
options: DailyReportOptions,
): Promise<DailyReportRow[]> {
const timezone = options.timezone;
const since = options.since;
const until = options.until;
const summaries: DailySummaries = new Map();

for (const event of events) {
const dateKey = toDateKey(event.timestamp, timezone);
if (!isWithinRange(dateKey, since, until)) {
continue;
}
accumulateDailyUsage(summaries, event, timezone);
}

return buildDailyReportRows(summaries, options);
}

if (import.meta.vitest != null) {
describe('buildDailyReport', () => {
it('aggregates events by day and calculates costs', async () => {
Expand Down
Loading