fix(plan): move the Copilot credits bar onto the estimate and print whole credits (#1199)

This commit is contained in:
iamtoruk 2026-08-31 03:10:29 -07:00
parent d879d6590b
commit 616a1fada2
7 changed files with 20 additions and 13 deletions

View file

@ -160,7 +160,8 @@ see the #927 ruling in `src/session-cache.ts`).
figure)`), and `codeburn status --format json | jq .plans.copilot` carries
`spentCredits` (exact only), `estimatedCredits` (exact plus estimate),
`creditRatedCalls` / `creditUnratedCalls`, `creditsIncomplete` and a plain
`creditsNote`. The bar and `percentUsed` deliberately stay on the exact figure.
`creditsNote`. The bar and `percentUsed` follow `estimatedCredits` while any
request is unrated, and the exact figure once every request carries one.
For the live authoritative number, use the menubar's GitHub quota endpoint
(separate system, PR #1200).
- **Sync.** `codeburn sync push` holds a copilot session until it has been

View file

@ -557,9 +557,9 @@ export function planBudgetHeadline(planUsage: PlanUsage): string {
if (unrated > 0) {
const rated = planUsage.creditRatedCalls ?? 0
const estimated = planUsage.estimatedCredits ?? spent
// Tenths, not the exact-figure precision: the bulk of this number is
// priced from tokens, so more digits would claim accuracy it lacks.
const shown = formatCredits(Math.round(estimated * 10) / 10)
// Whole credits only: the bulk of this number is priced from tokens,
// so decimals would claim accuracy it lacks.
const shown = formatCredits(Math.round(estimated))
return `${planLabel(planUsage)}: ~${shown} / ${formatCredits(budget)} AI Credits (estimated; ${rated} of ${rated + unrated} requests carry GitHub's exact figure)`
}
return `${planLabel(planUsage)}: ${formatCredits(spent)} / ${formatCredits(budget)} AI Credits`

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -152,7 +152,7 @@ export function copilotCreditsNote(rated: number, unrated: number): string {
if (unrated === 0) {
return `All ${rated} Copilot requests in this period carry GitHub's exact credit figure, so spentCredits is complete.`
}
return `${rated} of ${rated + unrated} Copilot requests carry GitHub's exact credit figure (Copilot CLI session-store rows are the only local source that has it). spentCredits counts only those; estimatedCredits adds the rest priced from tokens at listed API rates, which is a floor because request multipliers and cached-token pricing are not modelled. percentUsed still tracks spentCredits.`
return `${rated} of ${rated + unrated} Copilot requests carry GitHub's exact credit figure (Copilot CLI session-store rows are the only local source that has it). spentCredits counts only those; estimatedCredits adds the rest priced from tokens at listed API rates, which is a floor because request multipliers and cached-token pricing are not modelled. percentUsed tracks estimatedCredits while any request is unrated.`
}
export function projectMonthEnd(
@ -203,7 +203,11 @@ export function getPlanUsageFromProjects(plan: Plan, projects: ProjectSummary[],
if (isCopilotCreditsPlan(plan)) {
const { spentCredits, estimatedCredits, creditRatedCalls, creditUnratedCalls, creditsIncomplete } = copilotCreditSpend(projects)
const budgetCredits = plan.monthlyCredits ?? 0
const percentUsed = budgetCredits > 0 ? (spentCredits / budgetCredits) * 100 : 0
// The bar follows the estimate once any request lacks an exact figure:
// a bar at 0% beside a 19k real bill is the misleading picture #1199
// reported, and the estimate is a floor, so it under-alerts, never over.
const barCredits = creditUnratedCalls > 0 ? estimatedCredits : spentCredits
const percentUsed = budgetCredits > 0 ? (barCredits / budgetCredits) * 100 : 0
const spent = creditsToUsd(spentCredits)
const status: PlanStatus = percentUsed > 100 ? 'over' : percentUsed >= PLAN_NEAR_THRESHOLD_PCT ? 'near' : 'under'
const projectedMonthUsd = projectMonthEnd(projects, periodStart, periodEnd, today, spent, call => planCallSpend(plan, call))

View file

@ -102,7 +102,7 @@ describe('plan budget copy', () => {
creditUnratedCalls: 469,
})
expect(planBudgetHeadline(copilot))
.toBe("Copilot Max: ~9800.4 / 20000 AI Credits (estimated; 4 of 473 requests carry GitHub's exact figure)")
.toBe("Copilot Max: ~9800 / 20000 AI Credits (estimated; 4 of 473 requests carry GitHub's exact figure)")
expect(planBudgetHeadline(copilot)).not.toContain('—')
})

View file

@ -504,7 +504,7 @@ describe('copilot AI credit plan math', () => {
expect(usage.status).toBe('under')
})
it('does not fill the credits bar from token costUSD when nanoAiu is missing', () => {
it('fills the credits bar from the token estimate when nanoAiu is missing', () => {
const usage = getPlanUsageFromProjects(copilotPro, [
usageProject([
usageCall({
@ -517,7 +517,9 @@ describe('copilot AI credit plan math', () => {
expect(usage.spentCredits).toBe(0)
expect(usage.creditsIncomplete).toBe(true)
expect(usage.percentUsed).toBe(0)
// 42 USD at 0.01 USD per credit against the 1500-credit Pro budget.
expect(usage.percentUsed).toBe(280)
expect(usage.status).toBe('over')
expect(usage.spentApiEquivalentUsd).toBe(0)
})
@ -618,7 +620,7 @@ describe('copilot AI credit plan math', () => {
expect(spend.creditsIncomplete).toBe(true)
})
it('leaves the bar and percentUsed on the exact figure while exposing the estimate', () => {
it('moves the bar and percentUsed onto the estimate while exposing the exact figure', () => {
const usage = getPlanUsageFromProjects(copilotPro, [
usageProject([
usageCall({ provider: 'copilot', costUSD: 0.42, timestamp: '2026-08-05T12:00:00.000Z' }),
@ -626,7 +628,7 @@ describe('copilot AI credit plan math', () => {
], today)
expect(usage.spentCredits).toBe(0)
expect(usage.percentUsed).toBe(0)
expect(usage.percentUsed).toBeCloseTo(2.8, 10)
expect(usage.estimatedCredits).toBeCloseTo(42, 10)
expect(usage.creditUnratedCalls).toBe(1)
})