diff --git a/apps/opencode/src/commands/daily.ts b/apps/opencode/src/commands/daily.ts index 1ad9b1a8..b8088ed8 100644 --- a/apps/opencode/src/commands/daily.ts +++ b/apps/opencode/src/commands/daily.ts @@ -16,6 +16,27 @@ import { logger } from '../logger.ts'; const TABLE_COLUMN_COUNT = 8; +type TokenStats = { + inputTokens: number; + outputTokens: number; + cacheCreationTokens: number; + cacheReadTokens: number; + cost: number; +}; + +type ModelBreakdown = TokenStats & { + modelName: string; +}; + +function createModelBreakdowns(modelAggregates: Map): ModelBreakdown[] { + return Array.from(modelAggregates.entries()) + .map(([modelName, stats]) => ({ + modelName, + ...stats, + })) + .sort((a, b) => b.cost - a.cost); +} + export const dailyCommand = define({ name: 'daily', description: 'Show OpenCode token usage grouped by day', @@ -57,6 +78,7 @@ export const dailyCommand = define({ totalTokens: number; totalCost: number; modelsUsed: string[]; + modelBreakdowns: ModelBreakdown[]; }> = []; for (const [date, dayEntries] of Object.entries(entriesByDate)) { @@ -66,17 +88,47 @@ export const dailyCommand = define({ let cacheReadTokens = 0; let totalCost = 0; const modelsSet = new Set(); + const modelAggregates = new Map(); + const defaultStats: TokenStats = { + inputTokens: 0, + outputTokens: 0, + cacheCreationTokens: 0, + cacheReadTokens: 0, + cost: 0, + }; 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, fetcher); - modelsSet.add(entry.model); + const modelName = entry.model ?? 'unknown'; + if (modelName === '') { + continue; + } + + const entryInputTokens = entry.usage.inputTokens; + const entryOutputTokens = entry.usage.outputTokens; + const entryCacheCreation = entry.usage.cacheCreationInputTokens; + const entryCacheRead = entry.usage.cacheReadInputTokens; + const entryCost = await calculateCostForEntry(entry, fetcher); + + inputTokens += entryInputTokens; + outputTokens += entryOutputTokens; + cacheCreationTokens += entryCacheCreation; + cacheReadTokens += entryCacheRead; + totalCost += entryCost; + modelsSet.add(modelName); + + const existing = modelAggregates.get(modelName) ?? defaultStats; + + modelAggregates.set(modelName, { + inputTokens: existing.inputTokens + entryInputTokens, + outputTokens: existing.outputTokens + entryOutputTokens, + cacheCreationTokens: existing.cacheCreationTokens + entryCacheCreation, + cacheReadTokens: existing.cacheReadTokens + entryCacheRead, + cost: existing.cost + entryCost, + }); } const totalTokens = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens; + const modelBreakdowns = createModelBreakdowns(modelAggregates); dailyData.push({ date, @@ -87,6 +139,7 @@ export const dailyCommand = define({ totalTokens, totalCost, modelsUsed: Array.from(modelsSet), + modelBreakdowns, }); } diff --git a/apps/opencode/src/commands/monthly.ts b/apps/opencode/src/commands/monthly.ts index 453795c5..73820f79 100644 --- a/apps/opencode/src/commands/monthly.ts +++ b/apps/opencode/src/commands/monthly.ts @@ -16,6 +16,27 @@ import { logger } from '../logger.ts'; const TABLE_COLUMN_COUNT = 8; +type TokenStats = { + inputTokens: number; + outputTokens: number; + cacheCreationTokens: number; + cacheReadTokens: number; + cost: number; +}; + +type ModelBreakdown = TokenStats & { + modelName: string; +}; + +function createModelBreakdowns(modelAggregates: Map): ModelBreakdown[] { + return Array.from(modelAggregates.entries()) + .map(([modelName, stats]) => ({ + modelName, + ...stats, + })) + .sort((a, b) => b.cost - a.cost); +} + export const monthlyCommand = define({ name: 'monthly', description: 'Show OpenCode token usage grouped by month', @@ -57,6 +78,7 @@ export const monthlyCommand = define({ totalTokens: number; totalCost: number; modelsUsed: string[]; + modelBreakdowns: ModelBreakdown[]; }> = []; for (const [month, monthEntries] of Object.entries(entriesByMonth)) { @@ -66,17 +88,47 @@ export const monthlyCommand = define({ let cacheReadTokens = 0; let totalCost = 0; const modelsSet = new Set(); + const modelAggregates = new Map(); + const defaultStats: TokenStats = { + inputTokens: 0, + outputTokens: 0, + cacheCreationTokens: 0, + cacheReadTokens: 0, + cost: 0, + }; for (const entry of monthEntries) { - inputTokens += entry.usage.inputTokens; - outputTokens += entry.usage.outputTokens; - cacheCreationTokens += entry.usage.cacheCreationInputTokens; - cacheReadTokens += entry.usage.cacheReadInputTokens; - totalCost += await calculateCostForEntry(entry, fetcher); - modelsSet.add(entry.model); + const modelName = entry.model ?? 'unknown'; + if (modelName === '') { + continue; + } + + const entryInputTokens = entry.usage.inputTokens; + const entryOutputTokens = entry.usage.outputTokens; + const entryCacheCreation = entry.usage.cacheCreationInputTokens; + const entryCacheRead = entry.usage.cacheReadInputTokens; + const entryCost = await calculateCostForEntry(entry, fetcher); + + inputTokens += entryInputTokens; + outputTokens += entryOutputTokens; + cacheCreationTokens += entryCacheCreation; + cacheReadTokens += entryCacheRead; + totalCost += entryCost; + modelsSet.add(modelName); + + const existing = modelAggregates.get(modelName) ?? defaultStats; + + modelAggregates.set(modelName, { + inputTokens: existing.inputTokens + entryInputTokens, + outputTokens: existing.outputTokens + entryOutputTokens, + cacheCreationTokens: existing.cacheCreationTokens + entryCacheCreation, + cacheReadTokens: existing.cacheReadTokens + entryCacheRead, + cost: existing.cost + entryCost, + }); } const totalTokens = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens; + const modelBreakdowns = createModelBreakdowns(modelAggregates); monthlyData.push({ month, @@ -87,6 +139,7 @@ export const monthlyCommand = define({ totalTokens, totalCost, modelsUsed: Array.from(modelsSet), + modelBreakdowns, }); } diff --git a/apps/opencode/src/commands/session.ts b/apps/opencode/src/commands/session.ts index c36467c0..e34d3457 100644 --- a/apps/opencode/src/commands/session.ts +++ b/apps/opencode/src/commands/session.ts @@ -16,6 +16,27 @@ import { logger } from '../logger.ts'; const TABLE_COLUMN_COUNT = 8; +type TokenStats = { + inputTokens: number; + outputTokens: number; + cacheCreationTokens: number; + cacheReadTokens: number; + cost: number; +}; + +type ModelBreakdown = TokenStats & { + modelName: string; +}; + +function createModelBreakdowns(modelAggregates: Map): ModelBreakdown[] { + return Array.from(modelAggregates.entries()) + .map(([modelName, stats]) => ({ + modelName, + ...stats, + })) + .sort((a, b) => b.cost - a.cost); +} + export const sessionCommand = define({ name: 'session', description: 'Show OpenCode token usage grouped by session', @@ -62,6 +83,7 @@ export const sessionCommand = define({ totalTokens: number; totalCost: number; modelsUsed: string[]; + modelBreakdowns: ModelBreakdown[]; lastActivity: Date; }; @@ -74,15 +96,44 @@ export const sessionCommand = define({ let cacheReadTokens = 0; let totalCost = 0; const modelsSet = new Set(); + const modelAggregates = new Map(); + const defaultStats: TokenStats = { + inputTokens: 0, + outputTokens: 0, + cacheCreationTokens: 0, + cacheReadTokens: 0, + cost: 0, + }; let lastActivity = sessionEntries[0]!.timestamp; for (const entry of sessionEntries) { - inputTokens += entry.usage.inputTokens; - outputTokens += entry.usage.outputTokens; - cacheCreationTokens += entry.usage.cacheCreationInputTokens; - cacheReadTokens += entry.usage.cacheReadInputTokens; - totalCost += await calculateCostForEntry(entry, fetcher); - modelsSet.add(entry.model); + const modelName = entry.model ?? 'unknown'; + if (modelName === '') { + continue; + } + + const entryInputTokens = entry.usage.inputTokens; + const entryOutputTokens = entry.usage.outputTokens; + const entryCacheCreation = entry.usage.cacheCreationInputTokens; + const entryCacheRead = entry.usage.cacheReadInputTokens; + const entryCost = await calculateCostForEntry(entry, fetcher); + + inputTokens += entryInputTokens; + outputTokens += entryOutputTokens; + cacheCreationTokens += entryCacheCreation; + cacheReadTokens += entryCacheRead; + totalCost += entryCost; + modelsSet.add(modelName); + + const existing = modelAggregates.get(modelName) ?? defaultStats; + + modelAggregates.set(modelName, { + inputTokens: existing.inputTokens + entryInputTokens, + outputTokens: existing.outputTokens + entryOutputTokens, + cacheCreationTokens: existing.cacheCreationTokens + entryCacheCreation, + cacheReadTokens: existing.cacheReadTokens + entryCacheRead, + cost: existing.cost + entryCost, + }); if (entry.timestamp > lastActivity) { lastActivity = entry.timestamp; @@ -90,6 +141,7 @@ export const sessionCommand = define({ } const totalTokens = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens; + const modelBreakdowns = createModelBreakdowns(modelAggregates); const metadata = sessionMetadataMap.get(sessionID); @@ -104,6 +156,7 @@ export const sessionCommand = define({ totalTokens, totalCost, modelsUsed: Array.from(modelsSet), + modelBreakdowns, lastActivity, }); } diff --git a/apps/opencode/src/commands/weekly.ts b/apps/opencode/src/commands/weekly.ts index 011e8204..e7b20941 100644 --- a/apps/opencode/src/commands/weekly.ts +++ b/apps/opencode/src/commands/weekly.ts @@ -16,6 +16,27 @@ import { logger } from '../logger.ts'; const TABLE_COLUMN_COUNT = 8; +type TokenStats = { + inputTokens: number; + outputTokens: number; + cacheCreationTokens: number; + cacheReadTokens: number; + cost: number; +}; + +type ModelBreakdown = TokenStats & { + modelName: string; +}; + +function createModelBreakdowns(modelAggregates: Map): ModelBreakdown[] { + return Array.from(modelAggregates.entries()) + .map(([modelName, stats]) => ({ + modelName, + ...stats, + })) + .sort((a, b) => b.cost - a.cost); +} + /** * Get ISO week number for a date * ISO week starts on Monday, first week contains Jan 4th @@ -82,6 +103,7 @@ export const weeklyCommand = define({ totalTokens: number; totalCost: number; modelsUsed: string[]; + modelBreakdowns: ModelBreakdown[]; }> = []; for (const [week, weekEntries] of Object.entries(entriesByWeek)) { @@ -91,17 +113,47 @@ export const weeklyCommand = define({ let cacheReadTokens = 0; let totalCost = 0; const modelsSet = new Set(); + const modelAggregates = new Map(); + const defaultStats: TokenStats = { + inputTokens: 0, + outputTokens: 0, + cacheCreationTokens: 0, + cacheReadTokens: 0, + cost: 0, + }; for (const entry of weekEntries) { - inputTokens += entry.usage.inputTokens; - outputTokens += entry.usage.outputTokens; - cacheCreationTokens += entry.usage.cacheCreationInputTokens; - cacheReadTokens += entry.usage.cacheReadInputTokens; - totalCost += await calculateCostForEntry(entry, fetcher); - modelsSet.add(entry.model); + const modelName = entry.model ?? 'unknown'; + if (modelName === '') { + continue; + } + + const entryInputTokens = entry.usage.inputTokens; + const entryOutputTokens = entry.usage.outputTokens; + const entryCacheCreation = entry.usage.cacheCreationInputTokens; + const entryCacheRead = entry.usage.cacheReadInputTokens; + const entryCost = await calculateCostForEntry(entry, fetcher); + + inputTokens += entryInputTokens; + outputTokens += entryOutputTokens; + cacheCreationTokens += entryCacheCreation; + cacheReadTokens += entryCacheRead; + totalCost += entryCost; + modelsSet.add(modelName); + + const existing = modelAggregates.get(modelName) ?? defaultStats; + + modelAggregates.set(modelName, { + inputTokens: existing.inputTokens + entryInputTokens, + outputTokens: existing.outputTokens + entryOutputTokens, + cacheCreationTokens: existing.cacheCreationTokens + entryCacheCreation, + cacheReadTokens: existing.cacheReadTokens + entryCacheRead, + cost: existing.cost + entryCost, + }); } const totalTokens = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens; + const modelBreakdowns = createModelBreakdowns(modelAggregates); weeklyData.push({ week, @@ -112,6 +164,7 @@ export const weeklyCommand = define({ totalTokens, totalCost, modelsUsed: Array.from(modelsSet), + modelBreakdowns, }); }