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
7 changes: 3 additions & 4 deletions src/pipeline/aggregators/userUsageAggregator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getUsageMetrics, type TokenUsageHeader, type TokenUsageRecord } from '.
import { getDisplayModelName } from '../modelLabels'
import { getFriendlyProductName } from '../productClassification'
import { classifyUserSpendSegments, type UserSpendSegmentId } from '../../utils/userSpendSegments'
import { selectKnownMonthlyQuota } from '../aicIncludedCredits'

export type UserModelDailyUsage = {
requests: number
Expand Down Expand Up @@ -167,7 +168,7 @@ export class UserUsageAggregator implements Aggregator<TokenUsageRecord, UserUsa
if (!user) {
user = {
username,
totalMonthlyQuota: record.total_monthly_quota,
totalMonthlyQuota: selectKnownMonthlyQuota(0, record.total_monthly_quota),
organizations: new Set(),
costCenters: new Set(),
daily: new Map(),
Expand All @@ -186,9 +187,7 @@ export class UserUsageAggregator implements Aggregator<TokenUsageRecord, UserUsa
this.byUser.set(username, user)
}

if (record.total_monthly_quota > user.totalMonthlyQuota) {
user.totalMonthlyQuota = record.total_monthly_quota
}
user.totalMonthlyQuota = selectKnownMonthlyQuota(user.totalMonthlyQuota, record.total_monthly_quota)

const organization = record.organization.trim()
if (organization) {
Expand Down
69 changes: 69 additions & 0 deletions src/pipeline/aicIncludedCredits.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
PRO_PLUS_MONTHLY_AIC_INCLUDED_CREDITS,
PooledAicIncludedCreditsAllocator,
PRO_PLUS_MONTHLY_QUOTA,
selectKnownMonthlyQuota,
} from './aicIncludedCredits'
import { CostCenterAggregator } from './aggregators/costCenterAggregator'
import { OrganizationAggregator } from './aggregators/organizationAggregator'
Expand Down Expand Up @@ -45,6 +46,7 @@ const HEADER = [
'aic_quantity',
'aic_gross_amount',
].join(',')
const UNKNOWN_HIGH_MONTHLY_QUOTA = 2147483647

function createCsv(rows: string[][]): File {
const body = [HEADER, ...rows.map((row) => row.join(','))].join('\n')
Expand Down Expand Up @@ -170,6 +172,13 @@ describe('AIC included credit tiering and pool sizing', () => {
expect(getPlanLabel(0)).toBe('Unknown')
})

it('selects the maximum known monthly quota while ignoring unknown quota values', () => {
expect(selectKnownMonthlyQuota(0, UNKNOWN_HIGH_MONTHLY_QUOTA)).toBe(0)
expect(selectKnownMonthlyQuota(BUSINESS_MONTHLY_QUOTA, UNKNOWN_HIGH_MONTHLY_QUOTA)).toBe(BUSINESS_MONTHLY_QUOTA)
expect(selectKnownMonthlyQuota(UNKNOWN_HIGH_MONTHLY_QUOTA, ENTERPRISE_MONTHLY_QUOTA)).toBe(ENTERPRISE_MONTHLY_QUOTA)
expect(selectKnownMonthlyQuota(BUSINESS_MONTHLY_QUOTA, ENTERPRISE_MONTHLY_QUOTA)).toBe(ENTERPRISE_MONTHLY_QUOTA)
})

it('does not create an organization pool for a single-user Pro/Student report', async () => {
const file = createCsv([
['2026-03-01', 'mona', 'copilot', 'copilot_ai_credit', 'GPT-5', '10', 'ai-credits', '0.01', '0.10', '0', '0.10', 'False', '300', '', '', '10', '0.10'],
Expand Down Expand Up @@ -213,6 +222,32 @@ describe('AIC included credit tiering and pool sizing', () => {
)
})

it('ignores unknown high quota rows when sizing a business pool', async () => {
const file = createCsv([
['2026-03-01', 'mona', 'copilot', 'copilot_ai_credit', 'GPT-5', '10', 'ai-credits', '0.01', '0.10', '0', '0.10', 'False', '300', 'octo', 'Cats', '10', '0.10'],
['2026-03-02', 'mona', 'copilot', 'copilot_ai_credit', 'GPT-5', '10', 'ai-credits', '0.01', '0.10', '0', '0.10', 'False', `${UNKNOWN_HIGH_MONTHLY_QUOTA}`, 'octo', 'Cats', '10', '0.10'],
])

await expect(calculateAicIncludedCreditsPool(file)).resolves.toBe(BUSINESS_MONTHLY_AIC_INCLUDED_CREDITS)
})

it('ignores unknown high quota rows when sizing an enterprise pool', async () => {
const file = createCsv([
['2026-03-01', 'hubot', 'copilot', 'copilot_ai_credit', 'GPT-5', '10', 'ai-credits', '0.01', '0.10', '0', '0.10', 'False', '1000', 'octo', 'Cats', '10', '0.10'],
['2026-03-02', 'hubot', 'copilot', 'copilot_ai_credit', 'GPT-5', '10', 'ai-credits', '0.01', '0.10', '0', '0.10', 'False', `${UNKNOWN_HIGH_MONTHLY_QUOTA}`, 'octo', 'Cats', '10', '0.10'],
])

await expect(calculateAicIncludedCreditsPool(file)).resolves.toBe(ENTERPRISE_MONTHLY_AIC_INCLUDED_CREDITS)
})

it('does not size a pool from users that only have unknown high quota rows', async () => {
const file = createCsv([
['2026-03-01', 'mona', 'copilot', 'copilot_ai_credit', 'GPT-5', '10', 'ai-credits', '0.01', '0.10', '0', '0.10', 'False', `${UNKNOWN_HIGH_MONTHLY_QUOTA}`, 'octo', 'Cats', '10', '0.10'],
])

await expect(calculateAicIncludedCreditsPool(file)).resolves.toBe(0)
})

it('uses override seat counts instead of active users when sizing an organization pool', async () => {
const file = createCsv([
['2026-03-01', 'mona', 'copilot', 'copilot_ai_credit', 'GPT-5', '10', 'ai-credits', '0.01', '0.10', '0', '0.10', 'False', '300', 'example-org', 'Cost Center A', '10', '0.10'],
Expand Down Expand Up @@ -406,6 +441,40 @@ describe('AIC included credit tiering and pool sizing', () => {
}),
])
})

it('keeps user aggregation and license summary aligned when unknown high quota rows are present', async () => {
const file = createCsv([
['2026-03-01', 'mona', 'copilot', 'copilot_ai_credit', 'GPT-5', '1500', 'ai-credits', '0.01', '15.00', '0', '15.00', 'False', '300', 'example-org', 'Cost Center A', '1500', '15.00'],
['2026-03-02', 'mona', 'copilot', 'copilot_ai_credit', 'GPT-5', '1500', 'ai-credits', '0.01', '15.00', '0', '15.00', 'False', `${UNKNOWN_HIGH_MONTHLY_QUOTA}`, 'example-org', 'Cost Center A', '1500', '15.00'],
['2026-03-03', 'hubot', 'copilot', 'copilot_ai_credit', 'GPT-5', '3500', 'ai-credits', '0.01', '35.00', '0', '35.00', 'False', '1000', 'example-org', 'Cost Center A', '3500', '35.00'],
['2026-03-04', 'hubot', 'copilot', 'copilot_ai_credit', 'GPT-5', '3500', 'ai-credits', '0.01', '35.00', '0', '35.00', 'False', `${UNKNOWN_HIGH_MONTHLY_QUOTA}`, 'example-org', 'Cost Center A', '3500', '35.00'],
['2026-03-05', 'octocat', 'copilot', 'copilot_ai_credit', 'GPT-5', '100', 'ai-credits', '0.01', '1.00', '0', '1.00', 'False', `${UNKNOWN_HIGH_MONTHLY_QUOTA}`, 'example-org', 'Cost Center B', '100', '1.00'],
])
const users = new UserUsageAggregator()

await runPipeline(file, [users])

const userResult = users.result().users
const licenseSummary = calculateLicenseSummary(userResult)

expect(userResult.find((user) => user.username === 'mona')).toEqual(expect.objectContaining({
totalMonthlyQuota: BUSINESS_MONTHLY_QUOTA,
}))
expect(userResult.find((user) => user.username === 'hubot')).toEqual(expect.objectContaining({
totalMonthlyQuota: ENTERPRISE_MONTHLY_QUOTA,
}))
expect(userResult.find((user) => user.username === 'octocat')).toEqual(expect.objectContaining({
totalMonthlyQuota: 0,
}))
expect(licenseSummary).toEqual({
rows: [
{ label: 'Copilot Business', users: 1, includedAic: 3000 },
{ label: 'Copilot Enterprise', users: 1, includedAic: 7000 },
],
totalUsers: 2,
totalIncludedAic: 10000,
})
})
})

describe('pooled AIC allocation and derived AIC discounts', () => {
Expand Down
20 changes: 17 additions & 3 deletions src/pipeline/aicIncludedCredits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ export const BUSINESS_MONTHLY_QUOTA = 300
export const ENTERPRISE_MONTHLY_QUOTA = 1000
export const PRO_MONTHLY_QUOTA = 300
export const PRO_PLUS_MONTHLY_QUOTA = 1500
const KNOWN_MONTHLY_QUOTAS = new Set([
BUSINESS_MONTHLY_QUOTA,
ENTERPRISE_MONTHLY_QUOTA,
PRO_MONTHLY_QUOTA,
PRO_PLUS_MONTHLY_QUOTA,
])

export const BUSINESS_MONTHLY_AIC_INCLUDED_CREDITS = 3000
export const ENTERPRISE_MONTHLY_AIC_INCLUDED_CREDITS = 7000
Expand Down Expand Up @@ -70,6 +76,16 @@ function calculateOrganizationIncludedCreditsPool(overrides: AicIncludedCreditsO
)
}

export function isKnownMonthlyQuota(totalMonthlyQuota: number): boolean {
return Number.isFinite(totalMonthlyQuota) && KNOWN_MONTHLY_QUOTAS.has(totalMonthlyQuota)
}

export function selectKnownMonthlyQuota(currentQuota: number, candidateQuota: number): number {
const currentKnownQuota = isKnownMonthlyQuota(currentQuota) ? currentQuota : 0
if (!isKnownMonthlyQuota(candidateQuota)) return currentKnownQuota
return Math.max(currentKnownQuota, candidateQuota)
Comment thread
asizikov marked this conversation as resolved.
}

export function inferReportPlanScope(userCount: number, hasOrganizationContext = false): ReportPlanScope {
return userCount === 1 && !hasOrganizationContext ? 'individual' : 'organization'
}
Expand Down Expand Up @@ -200,9 +216,7 @@ export async function calculateAicIncludedCreditsContext(
}

const currentQuota = quotasByUser.get(username) ?? 0
if (record.total_monthly_quota > currentQuota) {
quotasByUser.set(username, record.total_monthly_quota)
}
quotasByUser.set(username, selectKnownMonthlyQuota(currentQuota, record.total_monthly_quota))
}

const reportPlanScope = inferReportPlanScope(quotasByUser.size, hasOrganizationContext)
Expand Down