Skip to content

Commit 3451141

Browse files
committed
feat: add data-aggregation module with helper functions
Create a new module to house common data aggregation functions that will be used to reduce code duplication in data-loader.ts. This module includes: - aggregateByModel() for model-based aggregation - aggregateModelBreakdowns() for breakdown aggregation - createModelBreakdowns() for formatting - calculateTotals() for token/cost totals - filterByDateRange() for date filtering - isDuplicateEntry() and markAsProcessed() for deduplication - extractUniqueModels() for unique model extraction
1 parent 292ede3 commit 3451141

1 file changed

Lines changed: 191 additions & 0 deletions

File tree

src/data-aggregation.ts

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import type { ModelBreakdown, UsageData } from './data-loader.ts';
2+
3+
export type TokenStats = {
4+
inputTokens: number;
5+
outputTokens: number;
6+
cacheCreationTokens: number;
7+
cacheReadTokens: number;
8+
cost: number;
9+
};
10+
11+
/**
12+
* Aggregates token counts and costs by model name
13+
*/
14+
export function aggregateByModel<T>(
15+
entries: T[],
16+
getModel: (entry: T) => string | undefined,
17+
getUsage: (entry: T) => UsageData['message']['usage'],
18+
getCost: (entry: T) => number,
19+
): Map<string, TokenStats> {
20+
const modelAggregates = new Map<string, TokenStats>();
21+
22+
for (const entry of entries) {
23+
const modelName = getModel(entry) ?? 'unknown';
24+
// Skip synthetic model
25+
if (modelName === '<synthetic>') {
26+
continue;
27+
}
28+
29+
const usage = getUsage(entry);
30+
const cost = getCost(entry);
31+
32+
const existing = modelAggregates.get(modelName) ?? {
33+
inputTokens: 0,
34+
outputTokens: 0,
35+
cacheCreationTokens: 0,
36+
cacheReadTokens: 0,
37+
cost: 0,
38+
};
39+
40+
modelAggregates.set(modelName, {
41+
inputTokens: existing.inputTokens + (usage.input_tokens ?? 0),
42+
outputTokens: existing.outputTokens + (usage.output_tokens ?? 0),
43+
cacheCreationTokens: existing.cacheCreationTokens + (usage.cache_creation_input_tokens ?? 0),
44+
cacheReadTokens: existing.cacheReadTokens + (usage.cache_read_input_tokens ?? 0),
45+
cost: existing.cost + cost,
46+
});
47+
}
48+
49+
return modelAggregates;
50+
}
51+
52+
/**
53+
* Aggregates model breakdowns from multiple sources
54+
*/
55+
export function aggregateModelBreakdowns(
56+
breakdowns: ModelBreakdown[],
57+
): Map<string, TokenStats> {
58+
const modelAggregates = new Map<string, TokenStats>();
59+
60+
for (const breakdown of breakdowns) {
61+
// Skip synthetic model
62+
if (breakdown.modelName === '<synthetic>') {
63+
continue;
64+
}
65+
66+
const existing = modelAggregates.get(breakdown.modelName) ?? {
67+
inputTokens: 0,
68+
outputTokens: 0,
69+
cacheCreationTokens: 0,
70+
cacheReadTokens: 0,
71+
cost: 0,
72+
};
73+
74+
modelAggregates.set(breakdown.modelName, {
75+
inputTokens: existing.inputTokens + breakdown.inputTokens,
76+
outputTokens: existing.outputTokens + breakdown.outputTokens,
77+
cacheCreationTokens: existing.cacheCreationTokens + breakdown.cacheCreationTokens,
78+
cacheReadTokens: existing.cacheReadTokens + breakdown.cacheReadTokens,
79+
cost: existing.cost + breakdown.cost,
80+
});
81+
}
82+
83+
return modelAggregates;
84+
}
85+
86+
/**
87+
* Converts model aggregates to sorted model breakdowns
88+
*/
89+
export function createModelBreakdowns(
90+
modelAggregates: Map<string, TokenStats>,
91+
): ModelBreakdown[] {
92+
return Array.from(modelAggregates.entries())
93+
.map(([modelName, stats]) => ({
94+
modelName,
95+
...stats,
96+
}))
97+
.sort((a, b) => b.cost - a.cost); // Sort by cost descending
98+
}
99+
100+
/**
101+
* Calculates total token counts and costs from entries
102+
*/
103+
export function calculateTotals<T>(
104+
entries: T[],
105+
getUsage: (entry: T) => UsageData['message']['usage'],
106+
getCost: (entry: T) => number,
107+
): TokenStats & { totalCost: number } {
108+
return entries.reduce(
109+
(acc, entry) => {
110+
const usage = getUsage(entry);
111+
const cost = getCost(entry);
112+
113+
return {
114+
inputTokens: acc.inputTokens + (usage.input_tokens ?? 0),
115+
outputTokens: acc.outputTokens + (usage.output_tokens ?? 0),
116+
cacheCreationTokens: acc.cacheCreationTokens + (usage.cache_creation_input_tokens ?? 0),
117+
cacheReadTokens: acc.cacheReadTokens + (usage.cache_read_input_tokens ?? 0),
118+
cost: acc.cost + cost,
119+
totalCost: acc.totalCost + cost,
120+
};
121+
},
122+
{
123+
inputTokens: 0,
124+
outputTokens: 0,
125+
cacheCreationTokens: 0,
126+
cacheReadTokens: 0,
127+
cost: 0,
128+
totalCost: 0,
129+
},
130+
);
131+
}
132+
133+
/**
134+
* Filters items by date range
135+
*/
136+
export function filterByDateRange<T>(
137+
items: T[],
138+
getDate: (item: T) => string,
139+
since?: string,
140+
until?: string,
141+
): T[] {
142+
if (since == null && until == null) {
143+
return items;
144+
}
145+
146+
return items.filter((item) => {
147+
const dateStr = getDate(item).replace(/-/g, ''); // Convert to YYYYMMDD
148+
if (since != null && dateStr < since) {
149+
return false;
150+
}
151+
if (until != null && dateStr > until) {
152+
return false;
153+
}
154+
return true;
155+
});
156+
}
157+
158+
/**
159+
* Checks if an entry is a duplicate based on hash
160+
*/
161+
export function isDuplicateEntry(
162+
uniqueHash: string | null,
163+
processedHashes: Set<string>,
164+
): boolean {
165+
if (uniqueHash == null) {
166+
return false;
167+
}
168+
return processedHashes.has(uniqueHash);
169+
}
170+
171+
/**
172+
* Marks an entry as processed
173+
*/
174+
export function markAsProcessed(
175+
uniqueHash: string | null,
176+
processedHashes: Set<string>,
177+
): void {
178+
if (uniqueHash != null) {
179+
processedHashes.add(uniqueHash);
180+
}
181+
}
182+
183+
/**
184+
* Extracts unique models from entries, excluding synthetic model
185+
*/
186+
export function extractUniqueModels<T>(
187+
entries: T[],
188+
getModel: (entry: T) => string | undefined,
189+
): string[] {
190+
return [...new Set(entries.map(getModel).filter((m): m is string => m != null && m !== '<synthetic>'))];
191+
}

0 commit comments

Comments
 (0)