From 286b5781a755a2238e2a8f0083ca58420715bd42 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Sun, 26 Apr 2026 15:31:32 -0400 Subject: [PATCH] fix: address analytics review follow-ups --- src/web-server/usage/aggregator.ts | 8 ++- src/web-server/usage/handlers.ts | 30 +++++++++++ ...ge-aggregator-cliproxy-integration.test.ts | 54 +++++++++++++++++++ 3 files changed, 90 insertions(+), 2 deletions(-) diff --git a/src/web-server/usage/aggregator.ts b/src/web-server/usage/aggregator.ts index 4f971696..34429df2 100644 --- a/src/web-server/usage/aggregator.ts +++ b/src/web-server/usage/aggregator.ts @@ -99,6 +99,10 @@ async function loadInstanceData(instancePath: string): Promise<{ } } +function getHourlyRequestCount(hour: HourlyUsage): number { + return hour.requestCount ?? hour.modelBreakdowns.length; +} + /** * Merge daily usage data from multiple sources * Combines entries with same date by aggregating tokens @@ -208,7 +212,7 @@ export function mergeHourlyData(sources: HourlyUsage[][]): HourlyUsage[] { existing.cacheCreationTokens += hour.cacheCreationTokens; existing.cacheReadTokens += hour.cacheReadTokens; existing.totalCost += hour.totalCost; - existing.requestCount = (existing.requestCount ?? 0) + (hour.requestCount ?? 0); + existing.requestCount = getHourlyRequestCount(existing) + getHourlyRequestCount(hour); const modelSet = new Set([...existing.modelsUsed, ...hour.modelsUsed]); existing.modelsUsed = Array.from(modelSet); // Merge model breakdowns @@ -231,7 +235,7 @@ export function mergeHourlyData(sources: HourlyUsage[][]): HourlyUsage[] { ...hour, modelsUsed: [...hour.modelsUsed], modelBreakdowns: hour.modelBreakdowns.map((b) => ({ ...b })), - requestCount: hour.requestCount ?? 0, + requestCount: getHourlyRequestCount(hour), }); } } diff --git a/src/web-server/usage/handlers.ts b/src/web-server/usage/handlers.ts index 489a1825..2f3bb054 100644 --- a/src/web-server/usage/handlers.ts +++ b/src/web-server/usage/handlers.ts @@ -694,6 +694,7 @@ export async function handleMonthly( cacheReadTokens: number; totalCost: number; modelsUsed: string[]; + modelBreakdowns: DailyUsage['modelBreakdowns']; }>; if (since || until) { @@ -708,6 +709,17 @@ export async function handleMonthly( cacheReadTokens: number; totalCost: number; modelsUsed: Set; + modelBreakdowns: Map< + string, + { + modelName: string; + inputTokens: number; + outputTokens: number; + cacheCreationTokens: number; + cacheReadTokens: number; + cost: number; + } + >; } >(); @@ -721,6 +733,7 @@ export async function handleMonthly( cacheReadTokens: 0, totalCost: 0, modelsUsed: new Set(), + modelBreakdowns: new Map(), }; existing.inputTokens += day.inputTokens; @@ -731,6 +744,22 @@ export async function handleMonthly( for (const model of day.modelsUsed) { existing.modelsUsed.add(model); } + for (const breakdown of day.modelBreakdowns) { + const existingBreakdown = existing.modelBreakdowns.get(breakdown.modelName) ?? { + modelName: breakdown.modelName, + inputTokens: 0, + outputTokens: 0, + cacheCreationTokens: 0, + cacheReadTokens: 0, + cost: 0, + }; + existingBreakdown.inputTokens += breakdown.inputTokens; + existingBreakdown.outputTokens += breakdown.outputTokens; + existingBreakdown.cacheCreationTokens += breakdown.cacheCreationTokens; + existingBreakdown.cacheReadTokens += breakdown.cacheReadTokens; + existingBreakdown.cost += breakdown.cost; + existing.modelBreakdowns.set(breakdown.modelName, existingBreakdown); + } monthMap.set(month, existing); } @@ -744,6 +773,7 @@ export async function handleMonthly( cacheReadTokens: month.cacheReadTokens, totalCost: month.totalCost, modelsUsed: Array.from(month.modelsUsed), + modelBreakdowns: Array.from(month.modelBreakdowns.values()), })) .sort((a, b) => a.month.localeCompare(b.month)); } else { diff --git a/tests/unit/web-server/usage-aggregator-cliproxy-integration.test.ts b/tests/unit/web-server/usage-aggregator-cliproxy-integration.test.ts index 1e887ee1..d82cb16e 100644 --- a/tests/unit/web-server/usage-aggregator-cliproxy-integration.test.ts +++ b/tests/unit/web-server/usage-aggregator-cliproxy-integration.test.ts @@ -306,4 +306,58 @@ describe('usage aggregator cliproxy integration', () => { expect(result[0].totalCost).toBe(3); expect(result[0].modelBreakdowns).toHaveLength(2); }); + + it('falls back to model cardinality when merging legacy hourly buckets without requestCount', () => { + const result = aggregator.mergeHourlyData([ + [ + { + hour: '2026-03-02 10:00', + source: 'legacy-a', + inputTokens: 10, + outputTokens: 1, + cacheCreationTokens: 0, + cacheReadTokens: 0, + cost: 1, + totalCost: 1, + modelsUsed: ['claude-sonnet-4-5'], + modelBreakdowns: [ + { + modelName: 'claude-sonnet-4-5', + inputTokens: 10, + outputTokens: 1, + cacheCreationTokens: 0, + cacheReadTokens: 0, + cost: 1, + }, + ], + }, + ], + [ + { + hour: '2026-03-02 10:00', + source: 'legacy-b', + inputTokens: 20, + outputTokens: 2, + cacheCreationTokens: 0, + cacheReadTokens: 0, + cost: 2, + totalCost: 2, + modelsUsed: ['gemini-2.5-pro'], + modelBreakdowns: [ + { + modelName: 'gemini-2.5-pro', + inputTokens: 20, + outputTokens: 2, + cacheCreationTokens: 0, + cacheReadTokens: 0, + cost: 2, + }, + ], + }, + ], + ]); + + expect(result).toHaveLength(1); + expect(result[0].requestCount).toBe(2); + }); });