Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,6 @@ function App() {
userAggregator,
]
}, {
enableNativeAiCreditsProcessing: true,
includedCreditsOverrides,
progressResolution: 500,
onProgress,
Expand Down
4 changes: 2 additions & 2 deletions src/pipeline/aggregators/usageMetrics.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { TokenUsageRecord } from '../parser'
import { getDefaultSupportedUsageReportAdapter, type ReportFormat, type ReportFormatMetadata } from '../reportAdapters'
import { getDefaultUsageReportAdapter, type ReportFormat, type ReportFormatMetadata } from '../reportAdapters'
import { getReportUsageMetrics } from '../reportUsageMetrics'

export type AggregatorUsageMetrics = {
Expand All @@ -16,7 +16,7 @@ export function getAggregatorReportFormat(
reportMetadataOrFormat?: ReportFormat | ReportFormatMetadata,
): ReportFormat {
if (!reportMetadataOrFormat) {
return getDefaultSupportedUsageReportAdapter().metadata.format
return getDefaultUsageReportAdapter().metadata.format
}

return typeof reportMetadataOrFormat === 'string'
Expand Down
1 change: 0 additions & 1 deletion src/pipeline/aicIncludedCredits.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ const NATIVE_AI_CREDITS_HEADER = [
const NATIVE_AI_CREDITS_REPORT_METADATA = {
format: 'native-ai-credits',
label: 'Native AI Credits report',
supported: false,
} as const
const UNKNOWN_HIGH_MONTHLY_QUOTA = 2147483647

Expand Down
2 changes: 0 additions & 2 deletions src/pipeline/includedCreditsPolicy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,11 @@ import {
const TRANSITION_PERIOD_REPORT_METADATA = {
format: 'transition-period-billing-preview',
label: 'Transition Period Billing Preview report',
supported: true,
} satisfies ReportFormatMetadata

const NATIVE_AI_CREDITS_REPORT_METADATA = {
format: 'native-ai-credits',
label: 'Native AI Credits report',
supported: false,
} satisfies ReportFormatMetadata

describe('resolveIncludedCreditsPolicy', () => {
Expand Down
71 changes: 5 additions & 66 deletions src/pipeline/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@ import {
parseNormalizedTokenUsageRecord,
parseTokenUsageHeader,
parseTokenUsageRecord,
UnsupportedNativeAiCreditsReportError,
UnsupportedReportVersionError,
PreAiCreditsReportVersionError,
validateHeader,
validateSupportedReportRecord,
} from './parser'

const FULL_HEADER = [
Expand Down Expand Up @@ -822,7 +820,7 @@ describe('validateHeader', () => {
expect(() => validateHeader(header)).toThrow(InvalidReportError)
})

it('throws UnsupportedReportVersionError when only aic columns are missing', () => {
it('throws PreAiCreditsReportVersionError when only aic columns are missing', () => {
const preAicHeader = [
'date',
'username',
Expand All @@ -841,10 +839,10 @@ describe('validateHeader', () => {
'cost_center_name',
].join(',')
const header = parseTokenUsageHeader(preAicHeader)
expect(() => validateHeader(header)).toThrow(UnsupportedReportVersionError)
expect(() => validateHeader(header)).toThrow(PreAiCreditsReportVersionError)
})

it('throws UnsupportedReportVersionError when only one aic column is missing', () => {
it('throws PreAiCreditsReportVersionError when only one aic column is missing', () => {
const preAicHeader = [
'date',
'username',
Expand All @@ -864,7 +862,7 @@ describe('validateHeader', () => {
'aic_quantity',
].join(',')
const header = parseTokenUsageHeader(preAicHeader)
expect(() => validateHeader(header)).toThrow(UnsupportedReportVersionError)
expect(() => validateHeader(header)).toThrow(PreAiCreditsReportVersionError)
})

it('throws InvalidReportError when a billing header omits a required non-aic billing column', () => {
Expand All @@ -891,62 +889,3 @@ describe('validateHeader', () => {
expect(() => validateHeader(header)).toThrow(InvalidReportError)
})
})

describe('validateSupportedReportRecord', () => {
it('throws a clear error for the native AI Credits report format', () => {
const header = parseTokenUsageHeader(HEADER_WITHOUT_EXCEEDS_QUOTA)
const record = parseTokenUsageRecord(
buildRow([
'5/29/26',
'mona',
'copilot',
'copilot_ai_credit',
'Auto: Claude Haiku 4.5',
'96.9990345',
'ai-credits',
'0.01',
'0.969990345',
'0',
'0.969990345',
'3900',
'example-org',
'',
'96.9990345',
'0.969990345',
]),
header,
)

expect(() => validateSupportedReportRecord(header, record)).toThrow(UnsupportedNativeAiCreditsReportError)
expect(() => validateSupportedReportRecord(header, record)).toThrow(
'currently supports PRU vs usage-based billing reports generated for the April and May billing periods',
)
})

it('accepts PRU report rows when exceeds_quota is absent', () => {
const header = parseTokenUsageHeader(HEADER_WITHOUT_EXCEEDS_QUOTA)
const record = parseTokenUsageRecord(
buildRow([
'2026-05-29',
'mona',
'copilot',
'copilot_premium_request',
'Auto: Claude Haiku 4.5',
'2',
'requests',
'0.04',
'0.08',
'0',
'0.08',
'300',
'example-org',
'Cost Center A',
'20',
'0.20',
]),
header,
)

expect(() => validateSupportedReportRecord(header, record)).not.toThrow()
})
})
23 changes: 3 additions & 20 deletions src/pipeline/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,24 +130,13 @@ export class InvalidReportError extends Error {
}
}

export class UnsupportedReportVersionError extends Error {
export class PreAiCreditsReportVersionError extends Error {
constructor() {
super(
`This report was exported before usage-based billing was introduced and cannot be displayed. ` +
`Please upload a more recent report that includes the AI Credits columns.`,
)
this.name = 'UnsupportedReportVersionError'
}
}

export class UnsupportedNativeAiCreditsReportError extends Error {
constructor() {
super(
`This billing preview app currently supports PRU vs usage-based billing reports generated for ` +
`the April and May billing periods. Reports generated on or after June 1 use AI Credits as ` +
`the primary unit and are not supported yet.`,
)
this.name = 'UnsupportedNativeAiCreditsReportError'
this.name = 'PreAiCreditsReportVersionError'
}
}

Expand All @@ -162,7 +151,7 @@ export function validateHeader(header: TokenUsageHeader): void {
validateBaseHeader(header)
const missingAic = REQUIRED_AIC_COLUMNS.filter((col) => !(col in header.index))
if (missingAic.length > 0) {
throw new UnsupportedReportVersionError()
throw new PreAiCreditsReportVersionError()
}
}

Expand All @@ -173,12 +162,6 @@ export function hasNativeAiCreditsReportSignature(header: TokenUsageHeader, reco
return lacksExceedsQuota && usesNativeAiCreditsUnit
}

export function validateSupportedReportRecord(header: TokenUsageHeader, record: TokenUsageRecord): void {
if (hasNativeAiCreditsReportSignature(header, record)) {
throw new UnsupportedNativeAiCreditsReportError()
}
}

function stripBom(s: string): string {
return s.replace(/^\uFEFF/, '')
}
Expand Down
22 changes: 12 additions & 10 deletions src/pipeline/reportAdapters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { describe, expect, it } from 'vitest'

import {
InvalidReportError,
UnsupportedNativeAiCreditsReportError,
UnsupportedReportVersionError,
PreAiCreditsReportVersionError,
parseTokenUsageHeader,
parseTokenUsageRecord,
} from './parser'
Expand Down Expand Up @@ -103,7 +102,6 @@ describe('usage report adapters', () => {
expect(detectReportFormat(header, record)).toBe('transition-period-billing-preview')
expect(selectUsageReportAdapter(header, record).metadata).toMatchObject({
format: 'transition-period-billing-preview',
supported: true,
})
expect(() => validateUsageReportFirstRecord(header, record)).not.toThrow()
})
Expand Down Expand Up @@ -137,6 +135,12 @@ describe('usage report adapters', () => {
expect(() => validateUsageReportFirstRecord(header, record)).not.toThrow()
})

it('uses the native adapter for header-only reports without exceeds_quota', () => {
const header = parseTokenUsageHeader(HEADER_WITHOUT_EXCEEDS_QUOTA)

expect(validateUsageReportHeader(header).metadata.format).toBe('native-ai-credits')
})

it('normalizes transition-period rows through the adapter parser', () => {
const header = parseTokenUsageHeader(TRANSITION_PERIOD_HEADER)
const adapter = validateUsageReportHeader(header)
Expand Down Expand Up @@ -196,7 +200,7 @@ describe('usage report adapters', () => {
})
})

it('detects native AI Credits reports and routes them to an unsupported adapter', () => {
it('detects native AI Credits reports and routes them to the native adapter', () => {
const header = parseTokenUsageHeader(HEADER_WITHOUT_EXCEEDS_QUOTA)
const row = buildRow([
'2026-06-01',
Expand Down Expand Up @@ -226,11 +230,9 @@ describe('usage report adapters', () => {
expect(detectReportFormat(header, record)).toBe('native-ai-credits')
expect(adapter.metadata).toMatchObject({
format: 'native-ai-credits',
supported: false,
})

expect(() => adapter.validateFirstRecord(header, record)).toThrow(UnsupportedNativeAiCreditsReportError)
expect(() => validateUsageReportFirstRecord(header, record)).toThrow(UnsupportedNativeAiCreditsReportError)
expect(() => validateUsageReportFirstRecord(header, record)).not.toThrow()
expect(adapter.parseRecord(row, header)).toMatchObject({
date: '2026-06-01',
quantity: 96.9990345,
Expand Down Expand Up @@ -267,7 +269,7 @@ describe('usage report adapters', () => {
expect(() => validateUsageReportHeader(header)).not.toThrow()
expect(detectReportFormat(header, record)).toBe('native-ai-credits')
expect(adapter.metadata.format).toBe('native-ai-credits')
expect(() => validateUsageReportFirstRecord(header, record, { allowUnsupportedNativeAiCredits: true })).not.toThrow()
expect(() => validateUsageReportFirstRecord(header, record)).not.toThrow()
expect(adapter.parseRecord(row, header)).toMatchObject({
date: '2026-06-01',
quantity: 42.726213,
Expand All @@ -280,7 +282,7 @@ describe('usage report adapters', () => {
})
})

it('normalizes native AI Credits dates through the unsupported adapter parser hook', () => {
it('normalizes native AI Credits dates through the native adapter parser hook', () => {
const header = parseTokenUsageHeader(HEADER_WITHOUT_EXCEEDS_QUOTA)
const row = buildRow([
'2026-06-01',
Expand Down Expand Up @@ -331,6 +333,6 @@ describe('usage report adapters', () => {
'cost_center_name',
].join(','))

expect(() => validateUsageReportHeader(header)).toThrow(UnsupportedReportVersionError)
expect(() => validateUsageReportHeader(header)).toThrow(PreAiCreditsReportVersionError)
})
})
30 changes: 3 additions & 27 deletions src/pipeline/reportAdapters.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import {
UnsupportedNativeAiCreditsReportError,
hasNativeAiCreditsReportSignature,
parseNativeAiCreditsUsageRecord,
parseNormalizedTokenUsageRecord,
validateBaseHeader,
validateHeader as validateTokenUsageHeader,
validateSupportedReportRecord,
type TokenUsageHeader,
type TokenUsageRecord,
} from './parser'
Expand All @@ -15,32 +13,22 @@ export type ReportFormat = 'transition-period-billing-preview' | 'native-ai-cred
export type ReportFormatMetadata = {
format: ReportFormat
label: string
supported: boolean
}

export interface UsageReportAdapter {
metadata: ReportFormatMetadata
validateHeader(header: TokenUsageHeader): void
validateFirstRecord(header: TokenUsageHeader, record: TokenUsageRecord): void
parseRecord(line: string, header: TokenUsageHeader): TokenUsageRecord | null
}

export interface UsageReportValidationOptions {
allowUnsupportedNativeAiCredits?: boolean
}

const TRANSITION_PERIOD_BILLING_PREVIEW_REPORT_ADAPTER: UsageReportAdapter = {
metadata: {
format: 'transition-period-billing-preview',
label: 'Transition Period Billing Preview report',
supported: true,
},
validateHeader(header) {
validateTokenUsageHeader(header)
},
validateFirstRecord(header, record) {
validateSupportedReportRecord(header, record)
},
parseRecord(line, header) {
return parseNormalizedTokenUsageRecord(line, header)
},
Expand All @@ -50,14 +38,10 @@ const NATIVE_AI_CREDITS_REPORT_ADAPTER: UsageReportAdapter = {
metadata: {
format: 'native-ai-credits',
label: 'Native AI Credits report',
supported: false,
},
validateHeader(header) {
validateBaseHeader(header)
},
validateFirstRecord() {
throw new UnsupportedNativeAiCreditsReportError()
},
parseRecord(line, header) {
return parseNativeAiCreditsUsageRecord(line, header)
},
Expand All @@ -68,23 +52,19 @@ const REPORT_ADAPTERS: Record<ReportFormat, UsageReportAdapter> = {
'native-ai-credits': NATIVE_AI_CREDITS_REPORT_ADAPTER,
}

function hasTransitionPeriodAicColumns(header: TokenUsageHeader): boolean {
return 'aic_quantity' in header.index && 'aic_gross_amount' in header.index
}

export function getDefaultSupportedUsageReportAdapter(): UsageReportAdapter {
export function getDefaultUsageReportAdapter(): UsageReportAdapter {
return TRANSITION_PERIOD_BILLING_PREVIEW_REPORT_ADAPTER
}

export function validateUsageReportHeader(header: TokenUsageHeader): UsageReportAdapter {
validateBaseHeader(header)

if (!('exceeds_quota' in header.index) && !hasTransitionPeriodAicColumns(header)) {
if (!('exceeds_quota' in header.index)) {
NATIVE_AI_CREDITS_REPORT_ADAPTER.validateHeader(header)
return NATIVE_AI_CREDITS_REPORT_ADAPTER
}

const adapter = getDefaultSupportedUsageReportAdapter()
const adapter = getDefaultUsageReportAdapter()
adapter.validateHeader(header)
return adapter
}
Expand All @@ -105,12 +85,8 @@ export function selectUsageReportAdapter(header: TokenUsageHeader, firstRecord:
export function validateUsageReportFirstRecord(
header: TokenUsageHeader,
firstRecord: TokenUsageRecord,
options?: UsageReportValidationOptions,
): UsageReportAdapter {
const adapter = selectUsageReportAdapter(header, firstRecord)
adapter.validateHeader(header)
if (!(options?.allowUnsupportedNativeAiCredits && adapter.metadata.format === 'native-ai-credits')) {
adapter.validateFirstRecord(header, firstRecord)
}
return adapter
}
1 change: 0 additions & 1 deletion src/pipeline/reportUsageMetrics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ const NATIVE_AI_CREDITS_HEADER = [
const TRANSITION_PERIOD_METADATA: ReportFormatMetadata = {
format: 'transition-period-billing-preview',
label: 'Transition Period Billing Preview report',
supported: true,
}

function buildRow(values: string[]): string {
Expand Down
Loading