Skip to content

Commit 7cee69d

Browse files
authored
Merge pull request #141 from github/asizikov/gated-native-pipeline
Gate native AI Credits pipeline processing
2 parents d5cfd70 + 544c33c commit 7cee69d

4 files changed

Lines changed: 218 additions & 6 deletions

File tree

src/pipeline/aicIncludedCredits.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
getAicUsageMetrics,
3+
parseNativeAiCreditsUsageRecord,
34
parseTokenUsageHeader,
45
parseNormalizedTokenUsageRecord,
56
type TokenUsageHeader,
@@ -254,6 +255,18 @@ function resolvePolicyForContext(
254255
return TRANSITION_PERIOD_INCLUDED_CREDITS_POLICY
255256
}
256257

258+
function parseIncludedCreditsRecord(
259+
line: string,
260+
header: TokenUsageHeader,
261+
options: AicIncludedCreditsProgressOptions | undefined,
262+
): TokenUsageRecord | null {
263+
if (options?.reportMetadata?.format === 'native-ai-credits') {
264+
return parseNativeAiCreditsUsageRecord(line, header)
265+
}
266+
267+
return parseNormalizedTokenUsageRecord(line, header)
268+
}
269+
257270
export function calculateLicenseSummary(
258271
users: Array<{ totalMonthlyQuota: number } & ReportScopeUser>,
259272
policy: IncludedCreditsPolicy = TRANSITION_PERIOD_INCLUDED_CREDITS_POLICY,
@@ -318,7 +331,7 @@ export async function calculateAicIncludedCreditsContext(
318331
continue
319332
}
320333

321-
const record = parseNormalizedTokenUsageRecord(trimmed, header)
334+
const record = parseIncludedCreditsRecord(trimmed, header, options)
322335
if (!record) continue
323336

324337
reportPeriod = includeDateInReportPeriod(reportPeriod, record.date)

src/pipeline/reportAdapters.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ export interface UsageReportAdapter {
2424
parseRecord(line: string, header: TokenUsageHeader): TokenUsageRecord | null
2525
}
2626

27+
export interface UsageReportValidationOptions {
28+
allowUnsupportedNativeAiCredits?: boolean
29+
}
30+
2731
const TRANSITION_PERIOD_BILLING_PREVIEW_REPORT_ADAPTER: UsageReportAdapter = {
2832
metadata: {
2933
format: 'transition-period-billing-preview',
@@ -89,9 +93,12 @@ export function selectUsageReportAdapter(header: TokenUsageHeader, firstRecord:
8993
export function validateUsageReportFirstRecord(
9094
header: TokenUsageHeader,
9195
firstRecord: TokenUsageRecord,
96+
options?: UsageReportValidationOptions,
9297
): UsageReportAdapter {
9398
const adapter = selectUsageReportAdapter(header, firstRecord)
9499
adapter.validateHeader(header)
95-
adapter.validateFirstRecord(header, firstRecord)
100+
if (!(options?.allowUnsupportedNativeAiCredits && adapter.metadata.format === 'native-ai-credits')) {
101+
adapter.validateFirstRecord(header, firstRecord)
102+
}
96103
return adapter
97104
}

src/pipeline/runPipeline.test.ts

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { describe, expect, it } from 'vitest'
22

33
import type { Aggregator } from './aggregators/base'
4+
import { DailyUsageAggregator } from './aggregators/dailyUsageAggregator'
5+
import { UserUsageAggregator } from './aggregators/userUsageAggregator'
46
import {
57
InvalidReportError,
68
UnsupportedNativeAiCreditsReportError,
@@ -55,6 +57,12 @@ const TRANSITION_PERIOD_REPORT_METADATA = {
5557
supported: true,
5658
}
5759

60+
const NATIVE_AI_CREDITS_REPORT_METADATA = {
61+
format: 'native-ai-credits',
62+
label: 'Native AI Credits report',
63+
supported: false,
64+
} as const
65+
5866
function createCsv(rows: string[][], header = HEADER): File {
5967
const body = [header, ...rows.map((row) => row.join(','))].join('\n')
6068
return new File([body], 'usage.csv', { type: 'text/csv' })
@@ -160,6 +168,181 @@ describe('runPipeline', () => {
160168
expect(aggregator.result()).toEqual([])
161169
})
162170

171+
it('processes native AI Credits reports with native metadata when explicitly enabled', async () => {
172+
const file = createCsv([
173+
[
174+
'5/29/26',
175+
'mona',
176+
'copilot',
177+
'copilot_ai_credit',
178+
'Auto: Claude Haiku 4.5',
179+
'96.9990345',
180+
'ai-credits',
181+
'0.01',
182+
'0.969990345',
183+
'0',
184+
'0.969990345',
185+
'3900',
186+
'example-org',
187+
'Cost Center A',
188+
'999',
189+
'999',
190+
],
191+
], NATIVE_AI_CREDITS_HEADER)
192+
const aggregator = new CaptureAggregator()
193+
194+
const result = await runPipeline(file, [aggregator], {
195+
enableNativeAiCreditsProcessing: true,
196+
})
197+
198+
expect(result).toEqual({
199+
reportMetadata: NATIVE_AI_CREDITS_REPORT_METADATA,
200+
reportRowCount: 1,
201+
processedRowCount: 1,
202+
})
203+
expect(aggregator.headerCalls()).toBe(1)
204+
expect(aggregator.result()).toEqual([
205+
expect.objectContaining({
206+
date: '2026-05-29',
207+
username: 'mona',
208+
quantity: 96.9990345,
209+
gross_amount: 0.969990345,
210+
net_amount: 0.969990345,
211+
aic_quantity: 96.9990345,
212+
aic_gross_amount: 0.969990345,
213+
has_aic_quantity: true,
214+
has_aic_gross_amount: true,
215+
}),
216+
])
217+
})
218+
219+
it('aggregates flagged native AI Credits rows with native-format aggregators', async () => {
220+
const file = createCsv([
221+
[
222+
'5/29/26',
223+
'mona',
224+
'copilot',
225+
'copilot_ai_credit',
226+
'GPT-5.2',
227+
'10',
228+
'ai-credits',
229+
'0.01',
230+
'100',
231+
'20',
232+
'80',
233+
'3900',
234+
'example-org',
235+
'Cost Center A',
236+
'999',
237+
'999',
238+
],
239+
[
240+
'05/29/2026',
241+
'hubot',
242+
'spark',
243+
'spark_ai_credit',
244+
'GPT-5.2',
245+
'25',
246+
'ai-credits',
247+
'0.01',
248+
'250',
249+
'50',
250+
'200',
251+
'7000',
252+
'octodemo',
253+
'Cost Center A',
254+
'',
255+
'',
256+
],
257+
], NATIVE_AI_CREDITS_HEADER)
258+
const daily = new DailyUsageAggregator(NATIVE_AI_CREDITS_REPORT_METADATA)
259+
const users = new UserUsageAggregator(NATIVE_AI_CREDITS_REPORT_METADATA)
260+
261+
await runPipeline(file, [daily, users], {
262+
enableNativeAiCreditsProcessing: true,
263+
})
264+
265+
expect(daily.result().dailyData).toEqual([
266+
expect.objectContaining({
267+
date: '2026-05-29',
268+
requests: 0,
269+
grossAmount: 0,
270+
discountAmount: 0,
271+
netAmount: 0,
272+
aicQuantity: 35,
273+
aicGrossAmount: 350,
274+
aicNetAmount: 280,
275+
}),
276+
])
277+
expect(users.result().users).toEqual([
278+
expect.objectContaining({
279+
username: 'hubot',
280+
totals: expect.objectContaining({
281+
requests: 0,
282+
grossAmount: 0,
283+
discountAmount: 0,
284+
netAmount: 0,
285+
aicQuantity: 25,
286+
aicGrossAmount: 250,
287+
aicNetAmount: 200,
288+
}),
289+
}),
290+
expect.objectContaining({
291+
username: 'mona',
292+
totals: expect.objectContaining({
293+
requests: 0,
294+
grossAmount: 0,
295+
discountAmount: 0,
296+
netAmount: 0,
297+
aicQuantity: 10,
298+
aicGrossAmount: 100,
299+
aicNetAmount: 80,
300+
}),
301+
}),
302+
])
303+
})
304+
305+
it('selects native summer and September included-credit policies for flagged native reports', async () => {
306+
const createNativePolicyCsv = (date: string) => createCsv([
307+
[
308+
date,
309+
'mona',
310+
'copilot',
311+
'copilot_ai_credit',
312+
'GPT-5.2',
313+
'5000',
314+
'ai-credits',
315+
'0.01',
316+
'50',
317+
'0',
318+
'50',
319+
'3900',
320+
'example-org',
321+
'Cost Center A',
322+
'5000',
323+
'50',
324+
],
325+
], NATIVE_AI_CREDITS_HEADER)
326+
const summerAggregator = new CaptureAggregator()
327+
const septemberAggregator = new CaptureAggregator()
328+
329+
await runPipeline(createNativePolicyCsv('8/31/26'), [summerAggregator], {
330+
enableNativeAiCreditsProcessing: true,
331+
})
332+
await runPipeline(createNativePolicyCsv('9/1/26'), [septemberAggregator], {
333+
enableNativeAiCreditsProcessing: true,
334+
})
335+
336+
expect(summerAggregator.result()[0]).toEqual(expect.objectContaining({
337+
date: '2026-08-31',
338+
aic_net_amount: 0,
339+
}))
340+
expect(septemberAggregator.result()[0]).toEqual(expect.objectContaining({
341+
date: '2026-09-01',
342+
}))
343+
expect(septemberAggregator.result()[0].aic_net_amount).toBeCloseTo(11)
344+
})
345+
163346
it('returns transition-period metadata while processing supported reports', async () => {
164347
const file = createCsv([
165348
['2026-04-25', 'mona', 'copilot', 'copilot_premium_request', 'GPT-5', '0', 'requests', '0.04', '0', '0', '0', 'False', '300', '', '', '0', '0'],

src/pipeline/runPipeline.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ import {
1212
validateUsageReportHeader,
1313
type ReportFormatMetadata,
1414
type UsageReportAdapter,
15+
type UsageReportValidationOptions,
1516
} from './reportAdapters'
1617
import { streamLines, type StreamProgress } from './streamer'
1718

18-
async function validateFileFormat(file: File): Promise<UsageReportAdapter> {
19+
async function validateFileFormat(file: File, options?: UsageReportValidationOptions): Promise<UsageReportAdapter> {
1920
let header: TokenUsageHeader | null = null
2021
let selectedAdapter: UsageReportAdapter | null = null
2122

@@ -31,7 +32,7 @@ async function validateFileFormat(file: File): Promise<UsageReportAdapter> {
3132
continue
3233
}
3334

34-
return validateUsageReportFirstRecord(header, parseTokenUsageRecord(trimmed, header))
35+
return validateUsageReportFirstRecord(header, parseTokenUsageRecord(trimmed, header), options)
3536
}
3637

3738
if (!selectedAdapter) {
@@ -50,6 +51,7 @@ export interface PipelineProgress {
5051
}
5152

5253
export interface PipelineOptions {
54+
enableNativeAiCreditsProcessing?: boolean
5355
includedCreditsOverrides?: AicIncludedCreditsOverrides
5456
progressResolution?: number
5557
onProgress?: (progress: PipelineProgress) => void
@@ -89,8 +91,15 @@ export async function runPipeline(
8991
aggregators: Aggregator<TokenUsageRecord, unknown, TokenUsageHeader>[],
9092
options?: PipelineOptions,
9193
): Promise<PipelineResult> {
92-
const { includedCreditsOverrides = {}, progressResolution = 500, onProgress } = options ?? {}
93-
const reportAdapter = await validateFileFormat(file)
94+
const {
95+
enableNativeAiCreditsProcessing = false,
96+
includedCreditsOverrides = {},
97+
progressResolution = 500,
98+
onProgress,
99+
} = options ?? {}
100+
const reportAdapter = await validateFileFormat(file, {
101+
allowUnsupportedNativeAiCredits: enableNativeAiCreditsProcessing,
102+
})
94103
const reportMetadata = reportAdapter.metadata
95104
let lastProgressStage: PipelineProgress['stage'] | null = null
96105
let lastProgressPercent = -1

0 commit comments

Comments
 (0)