Skip to content

Commit 12e99cf

Browse files
asizikovCopilot
andcommitted
feat: add product usage aggregator
Aggregate per-product and per-product-model breakdowns for PRU and AIC metrics from the token-based billing CSV report. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ebf04b4 commit 12e99cf

2 files changed

Lines changed: 91 additions & 0 deletions

File tree

src/pipeline/aggregators/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ export { DailyUsageAggregator } from './dailyUsageAggregator'
66
export type { DailyUsageData, DailyUsageResult } from './dailyUsageAggregator'
77
export { ModelUsageAggregator } from './modelUsageAggregator'
88
export type { ModelDailyUsageData, ModelUsageResult, ModelUsageTotals } from './modelUsageAggregator'
9+
export { ProductUsageAggregator } from './productUsageAggregator'
10+
export type { ProductUsage, ProductUsageResult, ProductUsageTotals } from './productUsageAggregator'
911
export { CostCenterAggregator } from './costCenterAggregator'
1012
export type { CostCenterResult, CostCenterUsage, CostTotals } from './costCenterAggregator'
1113
export { OrganizationAggregator } from './organizationAggregator'
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import type { Aggregator } from './base'
2+
import { getUsageMetrics, type TokenUsageHeader, type TokenUsageRecord } from '../parser'
3+
4+
export type ProductUsageTotals = {
5+
requests: number
6+
aiuQuantity: number
7+
grossAmount: number
8+
aiuGrossAmount: number
9+
}
10+
11+
export type ProductUsage = {
12+
product: string
13+
totals: ProductUsageTotals
14+
models: Record<string, ProductUsageTotals>
15+
}
16+
17+
export type ProductUsageResult = {
18+
products: ProductUsage[]
19+
}
20+
21+
function createTotals(): ProductUsageTotals {
22+
return {
23+
requests: 0,
24+
aiuQuantity: 0,
25+
grossAmount: 0,
26+
aiuGrossAmount: 0,
27+
}
28+
}
29+
30+
function getFriendlyProductName(record: TokenUsageRecord): string {
31+
const model = record.model.trim()
32+
33+
if (model === 'Code Review model') return 'Code Review'
34+
if (model === 'Coding Agent model') return 'Coding Agent'
35+
return 'Copilot'
36+
}
37+
38+
export class ProductUsageAggregator implements Aggregator<TokenUsageRecord, ProductUsageResult, TokenUsageHeader> {
39+
private readonly byProduct = new Map<string, { totals: ProductUsageTotals; models: Map<string, ProductUsageTotals> }>()
40+
41+
onHeader(): void {
42+
// header is intentionally ignored (we rely on parsed TokenUsageRecord fields)
43+
}
44+
45+
accumulate(record: TokenUsageRecord): void {
46+
const product = getFriendlyProductName(record)
47+
const model = record.model.trim() || 'Unknown'
48+
49+
let productUsage = this.byProduct.get(product)
50+
if (!productUsage) {
51+
productUsage = {
52+
totals: createTotals(),
53+
models: new Map(),
54+
}
55+
this.byProduct.set(product, productUsage)
56+
}
57+
58+
const { requests, aiuQuantity, grossAmount, aiuGrossAmount } = getUsageMetrics(record)
59+
productUsage.totals.requests += requests
60+
productUsage.totals.aiuQuantity += aiuQuantity
61+
productUsage.totals.grossAmount += grossAmount
62+
productUsage.totals.aiuGrossAmount += aiuGrossAmount
63+
64+
let modelTotals = productUsage.models.get(model)
65+
if (!modelTotals) {
66+
modelTotals = createTotals()
67+
productUsage.models.set(model, modelTotals)
68+
}
69+
70+
modelTotals.requests += requests
71+
modelTotals.aiuQuantity += aiuQuantity
72+
modelTotals.grossAmount += grossAmount
73+
modelTotals.aiuGrossAmount += aiuGrossAmount
74+
}
75+
76+
result(): ProductUsageResult {
77+
const products = Array.from(this.byProduct.entries())
78+
.map<ProductUsage>(([product, usage]) => ({
79+
product,
80+
totals: usage.totals,
81+
models: Object.fromEntries(
82+
Array.from(usage.models.entries()).sort((a, b) => b[1].grossAmount - a[1].grossAmount),
83+
),
84+
}))
85+
.sort((a, b) => b.totals.grossAmount - a.totals.grossAmount)
86+
87+
return { products }
88+
}
89+
}

0 commit comments

Comments
 (0)