Skip to content

Commit b715bd0

Browse files
authored
Merge pull request #19 from github/ui/users-license-summary
Add users license summary table
2 parents 3a3d318 + ab3247a commit b715bd0

20 files changed

Lines changed: 305 additions & 215 deletions

README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,11 @@ npm run preview
6060
3. The app will parse the report and compare request-based and AI Credits-based billing signals
6161
4. Explore the visualizations and data views to preview how future bills may look after the AI Credits transition
6262

63-
Sample reports are available in the `examples/` directory. Older examples without `aiu_quantity` still load, but AI Credits will remain `0` for those rows.
63+
Sample reports are available in the `examples/` directory. Older examples without `aic_quantity` still load, but AI Credits will remain `0` for those rows.
6464

6565
## CSV Report Format
6666

67-
The application expects CSV files with the following primary columns:
67+
The application expects CSV files with the following required columns:
6868

6969
- `date` - ISO date (YYYY-MM-DD)
7070
- `username` - GitHub username
@@ -81,9 +81,13 @@ The application expects CSV files with the following primary columns:
8181
- `total_monthly_quota` - Monthly quota amount
8282
- `organization` - Organization slug
8383
- `cost_center_name` - Optional cost center identifier
84-
- `aiu_quantity` - AI Credits for the same usage row
8584

86-
The viewer now supports both billing units. On `requests` rows, `quantity` and `applied_cost_per_quantity` drive PRU-based calculations, while `aiu_quantity` and `aiu_gross_amount` remain comparison values and stay `0` when those AI Credit fields are absent. On `ai-units` rows, Premium Requests and generic PRU gross/discount/net totals are counted as `0`; AI Credits are counted from the AI-specific credit fields (`aiu_quantity`, falling back to `quantity` only when the AI field is absent) and AI Credit gross cost comes from `aiu_gross_amount` (falling back to `gross_amount` only when the AI field is absent). Legacy token columns may still appear in older exports, but they are no longer used by the app.
85+
Newer exports may also include these AI Credit columns:
86+
87+
- `aic_quantity` - AI Credits for the same usage row
88+
- `aic_gross_amount` - AI Credit gross cost for the same usage row
89+
90+
The viewer now supports both billing units. On `requests` rows, `quantity` and `applied_cost_per_quantity` drive PRU-based calculations, while `aic_quantity` and `aic_gross_amount` remain comparison values and stay `0` when those AI Credit fields are absent. On `ai-units` rows, Premium Requests and generic PRU gross/discount/net totals are counted as `0`; AI Credits are counted from the AI-specific credit fields (`aic_quantity`, falling back to `quantity` only when the AI field is absent) and AI Credit gross cost comes from `aic_gross_amount` (falling back to `gross_amount` only when the AI field is absent).
8791

8892
See [docs/report-format.md](docs/report-format.md) for detailed format specifications.
8993

docs/report-format.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Source examples: files in `examples/`. Some existing sample exports still use the older token-oriented schema.
44

5-
## Primary columns used by the app
5+
## Required columns used by the app
66

77
| Column | Type | Description |
88
| --- | --- | --- |
@@ -21,21 +21,20 @@ Source examples: files in `examples/`. Some existing sample exports still use th
2121
| `total_monthly_quota` | number | Monthly quota for the user or plan applicable to the row. |
2222
| `organization` | string | Organization slug associated with the usage. |
2323
| `cost_center_name` | string | Optional cost center or tagging field. |
24-
| `aiu_quantity` | number | Same usage converted to AI Credits. |
25-
26-
## Additional columns
24+
## Optional AI Credit columns
2725

2826
| Column | Status | Notes |
2927
| --- | --- | --- |
30-
| `aiu_gross_amount` | Present in newer previews | Used for AI Credit gross cost when present. |
28+
| `aic_quantity` | Present in newer previews | Same usage converted to AI Credits. |
29+
| `aic_gross_amount` | Present in newer previews | Used for AI Credit gross cost when present. |
3130
| `total_input_tokens` | Legacy / optional | Older token-oriented export field; ignored by the app. |
3231
| `total_output_tokens` | Legacy / optional | Older token-oriented export field; ignored by the app. |
3332
| `total_cache_creation_tokens` | Legacy / optional | Older token-oriented export field; ignored by the app. |
3433
| `total_cache_read_tokens` | Legacy / optional | Older token-oriented export field; ignored by the app. |
3534

3635
## Notes
3736
- Header row is single-lined; subsequent rows are usage records. Values are comma-separated, double-quoted where needed.
38-
- Monetary fields are decimals. `aiu_quantity` may also be fractional.
37+
- Monetary fields are decimals. `aic_quantity` may also be fractional.
3938
- The app streams the file, treats the first row as the header, and counts subsequent data rows without loading the entire file into memory.
40-
- Current calculations support both row types: on `requests` rows, `quantity` and `applied_cost_per_quantity` describe PRU usage while `aiu_quantity` and `aiu_gross_amount` remain reference/comparison values and stay `0` when the AI Credit fields are absent; on `ai-units` rows, Premium Requests and generic PRU gross/discount/net totals are counted as `0`, while AI Credits and AI Credit gross cost are taken from the AI-specific fields (`aiu_quantity` / `aiu_gross_amount`, with fallback to `quantity` / `gross_amount` only when those AI-specific fields are absent).
41-
- Reports without `aiu_quantity` still load, but AI Credit summaries will display `0`.
39+
- Current calculations support both row types: on `requests` rows, `quantity` and `applied_cost_per_quantity` describe PRU usage while `aic_quantity` and `aic_gross_amount` remain reference/comparison values and stay `0` when those AI Credit fields are absent; on `ai-units` rows, Premium Requests and generic PRU gross/discount/net totals are counted as `0`, while AI Credits and AI Credit gross cost are taken from the AI-specific fields (`aic_quantity` / `aic_gross_amount`, with fallback to `quantity` / `gross_amount` only when those AI-specific fields are absent).
40+
- Reports without `aic_quantity` still load, but AI Credit summaries will display `0`.

src/App.css

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,7 @@
435435
color: #6366f1;
436436
}
437437

438-
.aiu-callout {
438+
.aic-callout {
439439
font-size: 15px;
440440
font-weight: 700;
441441
background: #ffffff;
@@ -659,7 +659,7 @@
659659
color: #1f2328;
660660
}
661661

662-
.comparison-card__big-number--aiu {
662+
.comparison-card__big-number--aic {
663663
color: #2da44e;
664664
}
665665

@@ -1085,7 +1085,7 @@ td.cost-centers-view__num {
10851085
opacity: 0.7;
10861086
}
10871087

1088-
.products-view__legendSwatch--aiu {
1088+
.products-view__legendSwatch--aic {
10891089
background: #54aeff;
10901090
}
10911091

@@ -1156,7 +1156,7 @@ td.cost-centers-view__num {
11561156
background: rgba(207, 34, 46, 0.7);
11571157
}
11581158

1159-
.products-view__barFill--aiu {
1159+
.products-view__barFill--aic {
11601160
background: #54aeff;
11611161
}
11621162

@@ -1172,7 +1172,7 @@ td.cost-centers-view__num {
11721172
color: #cf222e;
11731173
}
11741174

1175-
.products-view__barValue--aiu {
1175+
.products-view__barValue--aic {
11761176
color: #0969da;
11771177
}
11781178

@@ -1281,6 +1281,24 @@ td.cost-centers-view__num {
12811281
color: #636c76;
12821282
}
12831283

1284+
.users-view__summaryWrap {
1285+
margin-bottom: 16px;
1286+
}
1287+
1288+
.users-view__summaryTable th.users-view__num {
1289+
text-align: right;
1290+
}
1291+
1292+
.users-view__license {
1293+
font-weight: 500;
1294+
color: #1f2328;
1295+
}
1296+
1297+
.users-view__license--total,
1298+
.users-view__num--total {
1299+
font-weight: 600;
1300+
}
1301+
12841302
.users-view__search {
12851303
border: 1px solid #d1d9e0;
12861304
border-radius: 6px;

src/App.tsx

Lines changed: 34 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -18,38 +18,26 @@ import { ProductUsageAggregator, type ProductUsageResult } from './pipeline/aggr
1818
import { CostCenterAggregator, type CostCenterResult } from './pipeline/aggregators/costCenterAggregator'
1919
import { OrganizationAggregator, type OrganizationResult } from './pipeline/aggregators/organizationAggregator'
2020
import { UserUsageAggregator, type UserUsageResult } from './pipeline/aggregators/userUsageAggregator'
21+
import { getMonthlyAicEntitlements } from './pipeline/aicEntitlements'
2122
import { runPipeline } from './pipeline/runPipeline'
2223
import { fillDataForRange } from './utils/fillDataForRange'
23-
import { formatAiu, formatUsd } from './utils/format'
24+
import { formatAic, formatUsd } from './utils/format'
2425

2526
type Status = 'idle' | 'processing' | 'done'
2627

2728
function createEmptyDailyUsage(date: string): DailyUsageData {
2829
return {
2930
date,
3031
requests: 0,
31-
aiuQuantity: 0,
32+
aicQuantity: 0,
3233
grossAmount: 0,
33-
aiuGrossAmount: 0,
34+
aicGrossAmount: 0,
3435
aicNetAmount: 0,
3536
discountAmount: 0,
3637
netAmount: 0,
3738
}
3839
}
3940

40-
// Current pooled AI Credits estimate based on Copilot plan assumptions in the billing export:
41-
// quota >= 300 => 3,000 included credits, quota >= 1,000 => 7,000 included credits.
42-
const INCLUDED_AIC_BY_MONTHLY_QUOTA = [
43-
{ minimumQuota: 1000, includedCredits: 7000 },
44-
{ minimumQuota: 300, includedCredits: 3000 },
45-
] as const
46-
47-
function getIncludedAicCredits(totalMonthlyQuota: number): number {
48-
return INCLUDED_AIC_BY_MONTHLY_QUOTA.find((plan) => totalMonthlyQuota >= plan.minimumQuota)?.includedCredits ?? 0
49-
}
50-
51-
52-
5341
function App() {
5442
const [status, setStatus] = useState<Status>('idle')
5543
const [quickStats, setQuickStats] = useState<QuickStatsResult | null>(null)
@@ -190,24 +178,24 @@ function App() {
190178
const overviewTotals = dailyUsageData.reduce(
191179
(totals, day) => {
192180
totals.requests += day.requests
193-
totals.aiuQuantity += day.aiuQuantity
181+
totals.aicQuantity += day.aicQuantity
194182
totals.grossAmount += day.grossAmount
195-
totals.aiuGrossAmount += day.aiuGrossAmount
183+
totals.aicGrossAmount += day.aicGrossAmount
196184
totals.discountAmount += day.discountAmount
197185
totals.netAmount += day.netAmount
198186
return totals
199187
},
200-
{ requests: 0, aiuQuantity: 0, grossAmount: 0, aiuGrossAmount: 0, aicNetAmount: 0, discountAmount: 0, netAmount: 0 },
188+
{ requests: 0, aicQuantity: 0, grossAmount: 0, aicGrossAmount: 0, aicNetAmount: 0, discountAmount: 0, netAmount: 0 },
201189
)
202190

203-
const pooledAicCredits = (userUsage?.users ?? []).reduce((sum, user) => sum + getIncludedAicCredits(user.totalMonthlyQuota), 0)
191+
const pooledAicCredits = (userUsage?.users ?? []).reduce((sum, user) => sum + getMonthlyAicEntitlements(user.totalMonthlyQuota), 0)
204192

205193
const aicDiscountRate =
206-
overviewTotals.aiuQuantity > 0
207-
? Math.min(pooledAicCredits / overviewTotals.aiuQuantity, 1)
194+
overviewTotals.aicQuantity > 0
195+
? Math.min(pooledAicCredits / overviewTotals.aicQuantity, 1)
208196
: 0
209-
const aicDiscount = aicDiscountRate * overviewTotals.aiuGrossAmount
210-
const aicNetAmount = Math.max(overviewTotals.aiuGrossAmount - aicDiscount, 0)
197+
const aicDiscount = aicDiscountRate * overviewTotals.aicGrossAmount
198+
const aicNetAmount = Math.max(overviewTotals.aicGrossAmount - aicDiscount, 0)
211199

212200
return (
213201
<div className={`app ${hasReport ? 'app--review' : ''}`}>
@@ -392,7 +380,7 @@ function App() {
392380
<div className="context-banner">
393381
<h2 className="context-banner__title">GitHub Copilot is moving to token-based billing</h2>
394382
<p className="context-banner__body">
395-
Starting May 1, 2026, Copilot usage will be measured in AI Credits (AICs) instead of Premium Requests (PRUs). <strong className="aiu-callout">1 AIC = $0.01.</strong> This preview shows what your usage would look like under the new pricing.
383+
Starting May 1, 2026, Copilot usage will be measured in AI Credits (AICs) instead of Premium Requests (PRUs). <strong className="aic-callout">1 AIC = $0.01.</strong> This preview shows what your usage would look like under the new pricing.
396384
</p>
397385
{fileName && (
398386
<p className="context-banner__note">
@@ -414,10 +402,23 @@ function App() {
414402
const periodLabel = rangeStart
415403
? new Date(rangeStart + 'T00:00:00').toLocaleString('en-US', { month: 'long', year: 'numeric' })
416404
: null
417-
return savings > 0 && periodLabel ? (
405+
return periodLabel ? (
418406
<p className="comparison-framing">
419-
Your <strong>{periodLabel}</strong> usage would cost{' '}
420-
<strong>{formatUsd(savings)} less</strong> under token-based pricing
407+
{savings > 0 ? (
408+
<>
409+
Your <strong>{periodLabel}</strong> usage would cost{' '}
410+
<strong>{formatUsd(savings)} less</strong> under token-based pricing
411+
</>
412+
) : savings < 0 ? (
413+
<>
414+
Your <strong>{periodLabel}</strong> usage would cost{' '}
415+
<strong>{formatUsd(Math.abs(savings))} more</strong> under token-based pricing
416+
</>
417+
) : (
418+
<>
419+
Your <strong>{periodLabel}</strong> usage cost would be the same under token-based pricing
420+
</>
421+
)}
421422
</p>
422423
) : null
423424
})()}
@@ -445,13 +446,13 @@ function App() {
445446
</div>
446447
<div className="card comparison-card">
447448
<div className="comparison-card__label">Token-based billing (AICs)</div>
448-
<div className="comparison-card__big-number comparison-card__big-number--aiu">{formatUsd(aicNetAmount)}</div>
449-
<div className="comparison-card__period">{formatAiu(overviewTotals.aiuQuantity)} AICs</div>
449+
<div className="comparison-card__big-number comparison-card__big-number--aic">{formatUsd(aicNetAmount)}</div>
450+
<div className="comparison-card__period">{formatAic(overviewTotals.aicQuantity)} AICs</div>
450451
<div className="comparison-card__rate">1 AIC = $0.01</div>
451452
<div className="comparison-card__breakdown">
452453
<div className="comparison-card__breakdown-row">
453454
<span>Gross cost</span>
454-
<span>{formatUsd(overviewTotals.aiuGrossAmount)}</span>
455+
<span>{formatUsd(overviewTotals.aicGrossAmount)}</span>
455456
</div>
456457
<div className="comparison-card__breakdown-row comparison-card__breakdown-row--muted">
457458
<span>Included credits discount</span>
@@ -479,7 +480,7 @@ function App() {
479480
{
480481
label: 'AI Credits',
481482
color: '#22c55e',
482-
data: filledDailyUsageData.map((day) => day.aiuQuantity),
483+
data: filledDailyUsageData.map((day) => day.aicQuantity),
483484
yAxisID: 'y1',
484485
},
485486
]}
@@ -498,7 +499,7 @@ function App() {
498499
{
499500
label: 'AIC Gross Cost',
500501
color: '#54aeff',
501-
data: filledDailyUsageData.map((day) => day.aiuGrossAmount),
502+
data: filledDailyUsageData.map((day) => day.aicGrossAmount),
502503
yAxisID: 'y',
503504
},
504505
]}

src/pipeline/aggregators/costCenterAggregator.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,17 @@ export type CostTotals = {
66
grossAmount: number
77
discountAmount: number
88
netAmount: number
9-
aiuQuantity: number
10-
aiuGrossAmount: number
9+
aicQuantity: number
10+
aicGrossAmount: number
1111
aicNetAmount: number
1212
}
1313

1414
export type CostCenterUserTotals = {
1515
requests: number
1616
grossAmount: number
1717
netAmount: number
18-
aiuQuantity: number
19-
aiuGrossAmount: number
18+
aicQuantity: number
19+
aicGrossAmount: number
2020
aicNetAmount: number
2121
}
2222

@@ -47,8 +47,8 @@ function createCostTotals(): CostTotals {
4747
grossAmount: 0,
4848
discountAmount: 0,
4949
netAmount: 0,
50-
aiuQuantity: 0,
51-
aiuGrossAmount: 0,
50+
aicQuantity: 0,
51+
aicGrossAmount: 0,
5252
aicNetAmount: 0,
5353
}
5454
}
@@ -64,7 +64,7 @@ function ensureTotals(map: Map<string, CostTotals>, key: string): CostTotals {
6464
function ensureUserTotals(map: Map<string, CostCenterUserTotals>, key: string): CostCenterUserTotals {
6565
const existing = map.get(key)
6666
if (existing) return existing
67-
const created: CostCenterUserTotals = { requests: 0, grossAmount: 0, netAmount: 0, aiuQuantity: 0, aiuGrossAmount: 0, aicNetAmount: 0 }
67+
const created: CostCenterUserTotals = { requests: 0, grossAmount: 0, netAmount: 0, aicQuantity: 0, aicGrossAmount: 0, aicNetAmount: 0 }
6868
map.set(key, created)
6969
return created
7070
}
@@ -97,32 +97,32 @@ export class CostCenterAggregator implements Aggregator<TokenUsageRecord, CostCe
9797

9898
if (username) costCenter.users.add(username)
9999

100-
const { requests, grossAmount, discountAmount, netAmount, aiuQuantity, aiuGrossAmount, aicNetAmount } = getUsageMetrics(record)
100+
const { requests, grossAmount, discountAmount, netAmount, aicQuantity, aicGrossAmount, aicNetAmount } = getUsageMetrics(record)
101101

102102
costCenter.totals.requests += requests
103103
costCenter.totals.grossAmount += grossAmount
104104
costCenter.totals.discountAmount += discountAmount
105105
costCenter.totals.netAmount += netAmount
106-
costCenter.totals.aiuQuantity += aiuQuantity
107-
costCenter.totals.aiuGrossAmount += aiuGrossAmount
106+
costCenter.totals.aicQuantity += aicQuantity
107+
costCenter.totals.aicGrossAmount += aicGrossAmount
108108
costCenter.totals.aicNetAmount += aicNetAmount
109109

110110
const byModel = ensureTotals(costCenter.totalsByModel, model)
111111
byModel.requests += requests
112112
byModel.grossAmount += grossAmount
113113
byModel.discountAmount += discountAmount
114114
byModel.netAmount += netAmount
115-
byModel.aiuQuantity += aiuQuantity
116-
byModel.aiuGrossAmount += aiuGrossAmount
115+
byModel.aicQuantity += aicQuantity
116+
byModel.aicGrossAmount += aicGrossAmount
117117
byModel.aicNetAmount += aicNetAmount
118118

119119
if (username) {
120120
const byUser = ensureUserTotals(costCenter.totalsByUser, username)
121121
byUser.requests += requests
122122
byUser.grossAmount += grossAmount
123123
byUser.netAmount += netAmount
124-
byUser.aiuQuantity += aiuQuantity
125-
byUser.aiuGrossAmount += aiuGrossAmount
124+
byUser.aicQuantity += aicQuantity
125+
byUser.aicGrossAmount += aicGrossAmount
126126
byUser.aicNetAmount += aicNetAmount
127127
}
128128
}

0 commit comments

Comments
 (0)