diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 2f8dea9f7..cbdaf6c5e 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,12 @@ ## [Unreleased] +### Fixed + +- Fixed Antigravity usage provider emitting one bar per model instead of deduplicating by tier — a single account's 15+ model entries now collapse to one bar per tier, matching the shared-quota reality of the upstream API. +- Fixed Antigravity usage reports missing `email` and `accountId` in metadata, so the `/usage` display and the deduplicator can associate reports with their credentials. +- Fixed usage-report dedup ignoring `projectId` for Google Cloud providers, preventing duplicate credential entries from being recognized as the same account. + ## [15.9.67] - 2026-06-06 ### Fixed diff --git a/packages/ai/src/auth-storage.ts b/packages/ai/src/auth-storage.ts index f03f7b810..35c9a8929 100644 --- a/packages/ai/src/auth-storage.ts +++ b/packages/ai/src/auth-storage.ts @@ -2295,6 +2295,8 @@ export class AuthStorage { if (report.provider === "openai-codex" || report.provider === "anthropic") { return identifiers.map(identifier => `${report.provider}:${identifier.toLowerCase()}`); } + const projectId = this.#getUsageReportMetadataValue(report, "projectId"); + if (projectId) identifiers.push(`project:${projectId}`); const accountId = this.#getUsageReportMetadataValue(report, "accountId"); if (accountId) identifiers.push(`account:${accountId}`); const account = this.#getUsageReportMetadataValue(report, "account"); diff --git a/packages/ai/src/usage/google-antigravity.ts b/packages/ai/src/usage/google-antigravity.ts index 26963d7b5..0a5ad1a2f 100644 --- a/packages/ai/src/usage/google-antigravity.ts +++ b/packages/ai/src/usage/google-antigravity.ts @@ -148,46 +148,78 @@ async function fetchAntigravityUsage(params: UsageFetchParams, ctx: UsageFetchCo } const data = (await response.json()) as AntigravityUsageResponse; - const limits: UsageLimit[] = []; + + // The API returns per-model quota entries, but quota is shared across + // models within the same tier. Deduplicate by (tier, windowId) so one + // account doesn't produce 15 redundant bars. + const deduped = new Map< + string, + { amount: UsageAmount; window: UsageWindow | undefined; tier: string | undefined } + >(); let earliestReset: number | undefined; - for (const [modelId, modelInfo] of Object.entries(data.models ?? {})) { + for (const [_modelId, modelInfo] of Object.entries(data.models ?? {})) { const quotaInfos = normalizeQuotaInfos(modelInfo); for (const quotaInfo of quotaInfos) { + if (quotaInfo.remainingFraction === undefined) continue; const amount = buildAmount(quotaInfo); const window = parseWindow(quotaInfo); if (window?.resetsAt) { earliestReset = earliestReset ? Math.min(earliestReset, window.resetsAt) : window.resetsAt; } - const labelBase = modelInfo.displayName || modelId; - const label = quotaInfo.tier ? `${labelBase} (${quotaInfo.tier})` : labelBase; + const tier = quotaInfo.tier ?? "default"; const windowId = window?.id ?? "default"; - limits.push({ - id: `${modelId}:${quotaInfo.tier ?? "default"}:${windowId}`, - label, - scope: { - provider: params.provider, - accountId: credential.accountId, - projectId: credential.projectId, - modelId, - tier: quotaInfo.tier, - windowId, - }, - window, - amount, - status: getUsageStatus(amount.remainingFraction), - }); + const key = `${tier}|${windowId}`; + const existing = deduped.get(key); + if ( + !existing || + (existing.amount.remainingFraction !== undefined && + amount.remainingFraction !== undefined && + amount.remainingFraction < existing.amount.remainingFraction) + ) { + deduped.set(key, { amount, window, tier: quotaInfo.tier }); + } } } + const limits: UsageLimit[] = []; + for (const [key, entry] of deduped) { + const [tier, windowId] = key.split("|") as [string, string]; + const label = entry.tier ?? "Usage"; + limits.push({ + id: `google-antigravity:${tier}:${windowId}`, + label, + scope: { + provider: params.provider, + accountId: credential.accountId, + projectId: credential.projectId, + tier: entry.tier ?? undefined, + windowId, + }, + window: entry.window, + amount: entry.amount, + status: getUsageStatus(entry.amount.remainingFraction), + }); + } + + limits.sort((a, b) => { + const aFraction = a.amount.remainingFraction ?? 1; + const bFraction = b.amount.remainingFraction ?? 1; + return aFraction - bFraction; + }); + + const metadata: UsageReport["metadata"] = { + endpoint: url, + projectId: credential.projectId, + }; + if (credential.email) metadata.email = credential.email; + if (credential.accountId) metadata.accountId = credential.accountId; + const report: UsageReport = { provider: params.provider, fetchedAt: nowMs, limits, - metadata: { - endpoint: url, - projectId: credential.projectId, - }, + metadata, raw: data, }; diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 33ff5aaaf..a90e895a4 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -3,6 +3,10 @@ ## [Unreleased] ## [15.9.69] - 2026-06-06 +### Fixed + +- Fixed `/usage` aggregate amount fallback using raw `limits.length` as account count — now counts unique `accountId` values from limit scopes, so N limits from a single account no longer display as "N accts". +- Fixed `/usage` account labeling falling back to "account N" for providers that use `projectId` as their primary identity (e.g. Google Antigravity, Gemini CLI) — `projectId` from report metadata is now considered before the generic fallback. ### Added diff --git a/packages/coding-agent/src/modes/controllers/command-controller.ts b/packages/coding-agent/src/modes/controllers/command-controller.ts index f1f857ecb..8d9388597 100644 --- a/packages/coding-agent/src/modes/controllers/command-controller.ts +++ b/packages/coding-agent/src/modes/controllers/command-controller.ts @@ -1272,6 +1272,8 @@ function formatAccountLabel(limit: UsageLimit, report: UsageReport, index: numbe if (email) return email; const accountId = (report.metadata?.accountId as string | undefined) ?? limit.scope.accountId; if (accountId) return accountId; + const projectId = report.metadata?.projectId as string | undefined; + if (projectId) return projectId; return `account ${index + 1}`; } @@ -1280,6 +1282,8 @@ function formatUnlimitedReportLabel(report: UsageReport, index: number): string if (email) return email; const accountId = report.metadata?.accountId as string | undefined; if (accountId) return accountId; + const projectId = report.metadata?.projectId as string | undefined; + if (projectId) return projectId; return `account ${index + 1}`; } @@ -1365,7 +1369,12 @@ function formatAggregateAmount(limits: UsageLimit[]): string { return `${formatNumber(remainingPct)}% free`; } - return `${limits.length} accts`; + // Count unique accounts from limit scopes — not limits.length. + const uniqueAccountIds = new Set( + limits.map(limit => limit.scope.accountId).filter((id): id is string => typeof id === "string" && id.length > 0), + ); + if (uniqueAccountIds.size === 0) return ""; + return `${uniqueAccountIds.size} ${uniqueAccountIds.size === 1 ? "acct" : "accts"}`; } function resolveResetRange(limits: UsageLimit[], nowMs: number): string | null { diff --git a/packages/coding-agent/src/slash-commands/helpers/usage-report.ts b/packages/coding-agent/src/slash-commands/helpers/usage-report.ts index bbef96730..71f184550 100644 --- a/packages/coding-agent/src/slash-commands/helpers/usage-report.ts +++ b/packages/coding-agent/src/slash-commands/helpers/usage-report.ts @@ -26,6 +26,8 @@ function formatUsageReportAccount(report: UsageReport, limit: UsageLimit, index: if (typeof email === "string" && email) return email; const accountId = report.metadata?.accountId ?? limit.scope.accountId; if (typeof accountId === "string" && accountId) return accountId; + const projectId = report.metadata?.projectId; + if (typeof projectId === "string" && projectId) return projectId; return `account ${index + 1}`; }