Skip to content

Commit 15dccbc

Browse files
authored
Merge pull request #38 from github/asizikov/update-report-validation
fix: accept reports without exceeds_quota
2 parents 6424395 + 0921432 commit 15dccbc

3 files changed

Lines changed: 110 additions & 13 deletions

File tree

src/pipeline/parser.test.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,25 @@ const FULL_HEADER = [
2929
'aic_gross_amount',
3030
].join(',')
3131

32+
const HEADER_WITHOUT_EXCEEDS_QUOTA = [
33+
'date',
34+
'username',
35+
'product',
36+
'sku',
37+
'model',
38+
'quantity',
39+
'unit_type',
40+
'applied_cost_per_quantity',
41+
'gross_amount',
42+
'discount_amount',
43+
'net_amount',
44+
'total_monthly_quota',
45+
'organization',
46+
'cost_center_name',
47+
'aic_quantity',
48+
'aic_gross_amount',
49+
].join(',')
50+
3251
function buildRow(values: string[]): string {
3352
return values.join(',')
3453
}
@@ -211,6 +230,79 @@ describe('parser and metric normalization', () => {
211230
}
212231
})
213232

233+
it('defaults exceeds_quota to false when the column is absent', () => {
234+
const header = parseTokenUsageHeader(HEADER_WITHOUT_EXCEEDS_QUOTA)
235+
const record = parseTokenUsageRecord(
236+
buildRow([
237+
'2026-03-01',
238+
'mona',
239+
'copilot',
240+
'copilot_premium_request',
241+
'Claude Sonnet 4.5',
242+
'2',
243+
'requests',
244+
'0.04',
245+
'0.08',
246+
'0.01',
247+
'0.07',
248+
'300',
249+
'octodemo',
250+
'Octocats',
251+
'1.5',
252+
'0.015',
253+
]),
254+
header,
255+
)
256+
257+
expect(record.exceeds_quota).toBe(false)
258+
})
259+
260+
it('maps ai credits columns by name when their order is reversed', () => {
261+
const reversedAicHeader = [
262+
'date',
263+
'username',
264+
'product',
265+
'sku',
266+
'model',
267+
'quantity',
268+
'unit_type',
269+
'applied_cost_per_quantity',
270+
'gross_amount',
271+
'discount_amount',
272+
'net_amount',
273+
'total_monthly_quota',
274+
'organization',
275+
'cost_center_name',
276+
'aic_gross_amount',
277+
'aic_quantity',
278+
].join(',')
279+
const header = parseTokenUsageHeader(reversedAicHeader)
280+
const record = parseTokenUsageRecord(
281+
buildRow([
282+
'2026-03-01',
283+
'mona',
284+
'copilot',
285+
'copilot_ai_unit',
286+
'Claude Sonnet 4.5',
287+
'50',
288+
'ai-units',
289+
'0.01',
290+
'0.50',
291+
'0',
292+
'0.50',
293+
'300',
294+
'octodemo',
295+
'Octocats',
296+
'0.75',
297+
'75',
298+
]),
299+
header,
300+
)
301+
302+
expect(record.aic_quantity).toBe(75)
303+
expect(record.aic_gross_amount).toBe(0.75)
304+
})
305+
214306
it('keeps PRU metrics for requests rows', () => {
215307
const header = parseTokenUsageHeader(FULL_HEADER)
216308
const record = parseTokenUsageRecord(
@@ -400,6 +492,11 @@ describe('validateHeader', () => {
400492
expect(() => validateHeader(header)).not.toThrow()
401493
})
402494

495+
it('accepts a header when exceeds_quota is absent', () => {
496+
const header = parseTokenUsageHeader(HEADER_WITHOUT_EXCEEDS_QUOTA)
497+
expect(() => validateHeader(header)).not.toThrow()
498+
})
499+
403500
it('throws InvalidReportError when core billing columns are missing', () => {
404501
const header = parseTokenUsageHeader('foo,bar,baz')
405502
expect(() => validateHeader(header)).toThrow(InvalidReportError)

src/pipeline/parser.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,6 @@ const BASE_BILLING_COLUMNS = [
111111
'gross_amount',
112112
'discount_amount',
113113
'net_amount',
114-
'exceeds_quota',
115114
'total_monthly_quota',
116115
'organization',
117116
] as const

src/views/ReportGuideView.tsx

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ const HEADER = [
1010
'gross_amount',
1111
'discount_amount',
1212
'net_amount',
13-
'exceeds_quota',
1413
'total_monthly_quota',
1514
'organization',
1615
'cost_center_name',
@@ -30,7 +29,6 @@ const SAMPLE_ROW_1 = [
3029
'0.036000000000000004',
3130
'0.036000000000000004',
3231
'0',
33-
'FALSE',
3432
'1000',
3533
'octodemo',
3634
'Octocats',
@@ -50,7 +48,6 @@ const SAMPLE_ROW_2 = [
5048
'0',
5149
'0',
5250
'0',
53-
'FALSE',
5451
'1000',
5552
'octodemo',
5653
'Octocats',
@@ -86,12 +83,12 @@ const SAMPLE_ROW_2_ANNOTATIONS: { index: number; label: string; note: string }[]
8683
note: '$0 — there is no PRU cost to discount.',
8784
},
8885
{
89-
index: 15,
86+
index: 14,
9087
label: 'aic_quantity',
9188
note: '≈ 24.9 AICs consumed. Under AI Credits-based billing, even Base Models consume AI Credits proportional to actual token usage.',
9289
},
9390
{
94-
index: 16,
91+
index: 15,
9592
label: 'aic_gross_amount',
9693
note: '≈ $0.249 (24.9 AICs × $0.01). This is what the same usage would cost under AI Credits-based billing.',
9794
},
@@ -133,17 +130,16 @@ const SAMPLE_ROW_1_ANNOTATIONS: { index: number; label: string; note: string }[]
133130
label: 'net_amount',
134131
note: '$0.00 — net PRU charge after quota discount. No additional cost was incurred for this request.',
135132
},
136-
{ index: 11, label: 'exceeds_quota', note: 'FALSE — usage stayed within the monthly quota.' },
137-
{ index: 12, label: 'total_monthly_quota', note: '1,000 PRUs — the monthly PRU quota for this user\'s plan.' },
138-
{ index: 13, label: 'organization', note: 'The GitHub organization slug: "octodemo".' },
139-
{ index: 14, label: 'cost_center_name', note: '"Octocats" — the cost center this user is assigned to.' },
133+
{ index: 11, label: 'total_monthly_quota', note: '1,000 PRUs — the monthly PRU quota for this user\'s plan.' },
134+
{ index: 12, label: 'organization', note: 'The GitHub organization slug: "octodemo".' },
135+
{ index: 13, label: 'cost_center_name', note: '"Octocats" — the cost center this user is assigned to.' },
140136
{
141-
index: 15,
137+
index: 14,
142138
label: 'aic_quantity',
143139
note: '≈ 3.107 AICs — the AI Credits-based billing equivalent of this request. AICs represent the actual tokens consumed.',
144140
},
145141
{
146-
index: 16,
142+
index: 15,
147143
label: 'aic_gross_amount',
148144
note: '≈ $0.031 (3.107 AICs × $0.01). Under AI Credits-based billing this request would cost roughly $0.03.',
149145
},
@@ -196,7 +192,12 @@ export function ReportGuideView() {
196192
<h2 className="text-lg font-semibold text-fg-default mb-2">Report Format</h2>
197193
<p className="text-fg-muted text-sm mb-7">
198194
Each row in the CSV export represents one unit of Copilot usage for a single user and model on a given day.
199-
Below are annotated examples showing what each field means.
195+
Below are annotated examples showing what each field means. Some older exports also include an
196+
<code className="mx-1 font-mono text-xs">exceeds_quota</code>
197+
column between
198+
<code className="mx-1 font-mono text-xs">net_amount</code>
199+
and
200+
<code className="mx-1 font-mono text-xs">total_monthly_quota</code>.
200201
</p>
201202

202203
<div className="bg-bg-default border border-border-default rounded-lg px-6 pt-5 pb-6 mb-6">

0 commit comments

Comments
 (0)