Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6fc11bb
fix(ccusage-opencode): silence logger in JSON mode
vedang Jan 28, 2026
40d53f6
fix(ccusage-opencode): Add support for --since and --until flags
vedang Jan 28, 2026
7367fe7
fix(ccusage-codex): rename costUSD to totalCost in totals dict
vedang Jan 28, 2026
eb0651d
fix(ccusage-codex): add `cacheCreationTokens` and `cacheReadTokens` f…
vedang Jan 28, 2026
cfa00d1
fix(mcp): align codex JSON parsing with codex CLI
vedang Jan 31, 2026
f4e8f68
fix(ccusage-pi): Add `totalTokens` to totals dict
vedang Jan 28, 2026
3ab2803
fix(ccusage-amp): include cache tokens in totalTokens
vedang Jan 31, 2026
9edca55
fix: normalize totals outputs across ccusage/codex/opencode/pi/amp
vedang Jan 28, 2026
d6a8b29
fix(codex): skip zero-usage deltas
vedang Feb 2, 2026
3630599
fix(ccusage): apply jq to empty session output
vedang Feb 2, 2026
b1128c6
fix(codex): use totalCost in monthly table
vedang Feb 2, 2026
c2cea10
fix(mcp): harden codex JSON parsing
vedang Feb 2, 2026
8b62e68
fix(opencode): validate filter dates in UTC
vedang Feb 2, 2026
c073b64
refactor(pi): simplify monthly totalTokens calculation
vedang Feb 2, 2026
19b85e0
docs: add helper docstrings for review
vedang Feb 2, 2026
f37de3f
fix(opencode): align cache input token fields
vedang Feb 5, 2026
4b4f921
fix(ccusage): honor config jq for session output
vedang Feb 5, 2026
ce9a6ed
test(codex): use valid model keys in fixtures
vedang Feb 5, 2026
f5c38f8
fix(codex): use totalCost for daily/session totals
vedang Feb 5, 2026
88ee7a2
refactor(codex): drop redundant cachedInputTokens
vedang Feb 5, 2026
575cae9
refactor(pi): tidy totals handling
vedang Feb 5, 2026
2bdf022
docs(ccusage): add missing JSDoc coverage
vedang Feb 5, 2026
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
23 changes: 17 additions & 6 deletions apps/amp/src/commands/daily.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,22 @@ export const dailyCommand = define({
const { events } = await loadAmpUsageEvents();

if (events.length === 0) {
const output = jsonOutput
? JSON.stringify({ daily: [], totals: null })
: 'No Amp usage data found.';
// eslint-disable-next-line no-console
console.log(output);
if (jsonOutput) {
const emptyTotals = {
inputTokens: 0,
outputTokens: 0,
cacheCreationTokens: 0,
cacheReadTokens: 0,
totalTokens: 0,
credits: 0,
totalCost: 0,
};
// eslint-disable-next-line no-console
console.log(JSON.stringify({ daily: [], totals: emptyTotals }, null, 2));
} else {
// eslint-disable-next-line no-console
console.log('No Amp usage data found.');
}
return;
}

Expand Down Expand Up @@ -98,7 +109,7 @@ export const dailyCommand = define({
modelsSet.add(event.model);
}

const totalTokens = inputTokens + outputTokens;
const totalTokens = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens;

dailyData.push({
date,
Expand Down
26 changes: 20 additions & 6 deletions apps/amp/src/commands/monthly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,25 @@ export const monthlyCommand = define({
const { events } = await loadAmpUsageEvents();

if (events.length === 0) {
const output = jsonOutput
? JSON.stringify({ monthly: [], totals: null })
: 'No Amp usage data found.';
// eslint-disable-next-line no-console
console.log(output);
if (jsonOutput) {
const emptyOutput = {
monthly: [],
totals: {
inputTokens: 0,
outputTokens: 0,
cacheCreationTokens: 0,
cacheReadTokens: 0,
totalTokens: 0,
credits: 0,
totalCost: 0,
},
};
// eslint-disable-next-line no-console
console.log(JSON.stringify(emptyOutput, null, 2));
} else {
// eslint-disable-next-line no-console
console.log('No Amp usage data found.');
}
return;
}

Expand Down Expand Up @@ -98,7 +112,7 @@ export const monthlyCommand = define({
modelsSet.add(event.model);
}

const totalTokens = inputTokens + outputTokens;
const totalTokens = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens;

monthlyData.push({
month,
Expand Down
23 changes: 17 additions & 6 deletions apps/amp/src/commands/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,22 @@ export const sessionCommand = define({
const { events, threads } = await loadAmpUsageEvents();

if (events.length === 0) {
const output = jsonOutput
? JSON.stringify({ sessions: [], totals: null })
: 'No Amp usage data found.';
// eslint-disable-next-line no-console
console.log(output);
if (jsonOutput) {
const emptyTotals = {
inputTokens: 0,
outputTokens: 0,
cacheCreationTokens: 0,
cacheReadTokens: 0,
totalTokens: 0,
credits: 0,
totalCost: 0,
};
// eslint-disable-next-line no-console
console.log(JSON.stringify({ sessions: [], totals: emptyTotals }, null, 2));
} else {
// eslint-disable-next-line no-console
console.log('No Amp usage data found.');
}
return;
}

Expand Down Expand Up @@ -104,7 +115,7 @@ export const sessionCommand = define({
}
}

const totalTokens = inputTokens + outputTokens;
const totalTokens = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens;
const threadInfo = threads.get(threadId);

sessionData.push({
Expand Down
3 changes: 2 additions & 1 deletion apps/amp/src/data-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ function convertLedgerEventToUsageEvent(
outputTokens,
cacheCreationInputTokens,
cacheReadInputTokens,
totalTokens: inputTokens + outputTokens,
totalTokens: inputTokens + outputTokens + cacheCreationInputTokens + cacheReadInputTokens,
};
}

Expand Down Expand Up @@ -330,6 +330,7 @@ if (import.meta.vitest != null) {
expect(event.outputTokens).toBe(50);
expect(event.cacheCreationInputTokens).toBe(500);
expect(event.cacheReadInputTokens).toBe(200);
expect(event.totalTokens).toBe(850);
expect(event.credits).toBe(1.5);

expect(threads.get('T-test-thread-123')).toEqual({
Expand Down
4 changes: 4 additions & 0 deletions apps/ccusage/src/_macro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ function isClaudeModel(modelName: string, _pricing: LiteLLMModelPricing): boolea
);
}

/**
* Fetch and cache Claude model pricing data from LiteLLM.
* @returns Pricing dataset filtered to Claude models
*/
export async function prefetchClaudePricing(): Promise<Record<string, LiteLLMModelPricing>> {
try {
const dataset = await fetchLiteLLMPricingDataset();
Expand Down
69 changes: 69 additions & 0 deletions apps/ccusage/src/_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,27 @@ export const modelNameSchema = v.pipe(
v.brand('ModelName'),
);

/**
* Schema for Claude session identifiers.
*/
export const sessionIdSchema = v.pipe(
v.string(),
v.minLength(1, 'Session ID cannot be empty'),
v.brand('SessionId'),
);

/**
* Schema for API request identifiers.
*/
export const requestIdSchema = v.pipe(
v.string(),
v.minLength(1, 'Request ID cannot be empty'),
v.brand('RequestId'),
);

/**
* Schema for Claude message identifiers.
*/
export const messageIdSchema = v.pipe(
v.string(),
v.minLength(1, 'Message ID cannot be empty'),
Expand All @@ -32,53 +41,77 @@ export const messageIdSchema = v.pipe(

// Date and timestamp schemas
const isoTimestampRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
/**
* Schema for ISO-8601 timestamps stored in usage entries.
*/
export const isoTimestampSchema = v.pipe(
v.string(),
v.regex(isoTimestampRegex, 'Invalid ISO timestamp'),
v.brand('ISOTimestamp'),
);

const yyyymmddRegex = /^\d{4}-\d{2}-\d{2}$/;
/**
* Schema for daily usage date keys (YYYY-MM-DD).
*/
export const dailyDateSchema = v.pipe(
v.string(),
v.regex(yyyymmddRegex, 'Date must be in YYYY-MM-DD format'),
v.brand('DailyDate'),
);

/**
* Schema for activity date values (YYYY-MM-DD).
*/
export const activityDateSchema = v.pipe(
v.string(),
v.regex(yyyymmddRegex, 'Date must be in YYYY-MM-DD format'),
v.brand('ActivityDate'),
);

const yyyymmRegex = /^\d{4}-\d{2}$/;
/**
* Schema for monthly usage date keys (YYYY-MM).
*/
export const monthlyDateSchema = v.pipe(
v.string(),
v.regex(yyyymmRegex, 'Date must be in YYYY-MM format'),
v.brand('MonthlyDate'),
);

/**
* Schema for weekly usage date keys (week start date).
*/
export const weeklyDateSchema = v.pipe(
v.string(),
v.regex(yyyymmddRegex, 'Date must be in YYYY-MM-DD format'),
v.brand('WeeklyDate'),
);

const filterDateRegex = /^\d{8}$/;
/**
* Schema for CLI filter dates in YYYYMMDD format.
*/
export const filterDateSchema = v.pipe(
v.string(),
v.regex(filterDateRegex, 'Date must be in YYYYMMDD format'),
v.brand('FilterDate'),
);

// Other domain-specific schemas
/**
* Schema for local project paths used to group sessions.
*/
export const projectPathSchema = v.pipe(
v.string(),
v.minLength(1, 'Project path cannot be empty'),
v.brand('ProjectPath'),
);

const versionRegex = /^\d+\.\d+\.\d+/;
/**
* Schema for Claude Code version strings.
*/
export const versionSchema = v.pipe(
v.string(),
v.regex(versionRegex, 'Invalid version format'),
Expand Down Expand Up @@ -107,22 +140,58 @@ export type Version = v.InferOutput<typeof versionSchema>;
* These functions should be used when converting plain strings to branded types
*/
export const createModelName = (value: string): ModelName => v.parse(modelNameSchema, value);
/**
* Parse and brand a session identifier string.
*/
export const createSessionId = (value: string): SessionId => v.parse(sessionIdSchema, value);
/**
* Parse and brand a request identifier string.
*/
export const createRequestId = (value: string): RequestId => v.parse(requestIdSchema, value);
/**
* Parse and brand a message identifier string.
*/
export const createMessageId = (value: string): MessageId => v.parse(messageIdSchema, value);
/**
* Parse and brand an ISO timestamp string.
*/
export function createISOTimestamp(value: string): ISOTimestamp {
return v.parse(isoTimestampSchema, value);
}
/**
* Parse and brand a daily date (YYYY-MM-DD).
*/
export const createDailyDate = (value: string): DailyDate => v.parse(dailyDateSchema, value);
/**
* Parse and brand an activity date (YYYY-MM-DD).
*/
export function createActivityDate(value: string): ActivityDate {
return v.parse(activityDateSchema, value);
}
/**
* Parse and brand a monthly date (YYYY-MM).
*/
export const createMonthlyDate = (value: string): MonthlyDate => v.parse(monthlyDateSchema, value);
/**
* Parse and brand a weekly date (week start).
*/
export const createWeeklyDate = (value: string): WeeklyDate => v.parse(weeklyDateSchema, value);
/**
* Parse and brand a filter date (YYYYMMDD).
*/
export const createFilterDate = (value: string): FilterDate => v.parse(filterDateSchema, value);
/**
* Parse and brand a project path string.
*/
export const createProjectPath = (value: string): ProjectPath => v.parse(projectPathSchema, value);
/**
* Parse and brand a Claude Code version string.
*/
export const createVersion = (value: string): Version => v.parse(versionSchema, value);

/**
* Parse a bucket key into a weekly or monthly date bucket.
*/
export function createBucket(value: string): Bucket {
const weeklyResult = v.safeParse(weeklyDateSchema, value);
if (weeklyResult.success) {
Expand Down
4 changes: 4 additions & 0 deletions apps/ccusage/src/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { stat, utimes, writeFile } from 'node:fs/promises';
import { Result } from '@praha/byethrow';
import { createFixture } from 'fs-fixture';

/**
* Exhaustive-check helper for unreachable code paths.
* @param value - Value that should never occur
*/
export function unreachable(value: never): never {
throw new Error(`Unreachable code reached with value: ${value as any}`);
}
Expand Down
3 changes: 3 additions & 0 deletions apps/ccusage/src/commands/blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ function parseTokenLimit(value: string | undefined, maxFromAll: number): number
return Number.isNaN(limit) ? undefined : limit;
}

/**
* CLI command for 5-hour billing block reports.
*/
export const blocksCommand = define({
name: 'blocks',
description: 'Show usage report grouped by session billing blocks',
Expand Down
15 changes: 14 additions & 1 deletion apps/ccusage/src/commands/daily.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import { loadDailyUsageData } from '../data-loader.ts';
import { detectMismatches, printMismatchReport } from '../debug.ts';
import { log, logger } from '../logger.ts';

/**
* CLI command for daily usage reports.
*/
export const dailyCommand = define({
name: 'daily',
description: 'Show usage report grouped by date',
Expand Down Expand Up @@ -82,7 +85,17 @@ export const dailyCommand = define({

if (dailyData.length === 0) {
if (useJson) {
log(JSON.stringify([]));
const totals = createTotalsObject({
inputTokens: 0,
outputTokens: 0,
cacheCreationTokens: 0,
cacheReadTokens: 0,
totalCost: 0,
});
const jsonOutput = mergedOptions.instances
? { projects: {}, totals }
: { daily: [], totals };
log(JSON.stringify(jsonOutput, null, 2));
} else {
logger.warn('No Claude usage data found.');
}
Expand Down
3 changes: 3 additions & 0 deletions apps/ccusage/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ for (const [name, command] of subCommandUnion) {
*/
const mainCommand = dailyCommand;

/**
* Execute the ccusage CLI with the configured subcommands.
*/
export async function run(): Promise<void> {
// When invoked through npx, the binary name might be passed as the first argument
// Filter it out if it matches the expected binary name
Expand Down
3 changes: 3 additions & 0 deletions apps/ccusage/src/commands/monthly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import { loadMonthlyUsageData } from '../data-loader.ts';
import { detectMismatches, printMismatchReport } from '../debug.ts';
import { log, logger } from '../logger.ts';

/**
* CLI command for monthly usage reports.
*/
export const monthlyCommand = define({
name: 'monthly',
description: 'Show usage report grouped by month',
Expand Down
Loading