-
Notifications
You must be signed in to change notification settings - Fork 1k
feat(quota): per-account Gem/Cla quota for Google Antigravity (#1082) #3213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| # wp11 — #1082 per-account Gem/Cla quota for Google Antigravity (reimplementation; PR #2123 closed) | ||
|
|
||
| Issue #1082 (score 63). PR #2123 (chilung-cgu, +755/-40, 319 commits behind, hygiene/enforce-target | ||
| red) carried two reviewer blockers across three rounds: (1) cache/in-flight identity ignores the | ||
| configurable Antigravity destination so a baseUrl change replays stale rows and stale writers can | ||
| publish across generations; (2) every stored account bearer goes out through plain `fetch` to a | ||
| configurable host without the repository's pinned provider-outbound transport. | ||
|
|
||
| ## Design (removes both blockers by construction) | ||
|
|
||
| - `supportsPerAccountQuota`: add `google-antigravity`. | ||
| - `fetchAccountQuota`: branch for `google-antigravity` → `fetchAntigravityUsageQuota(token, projectId)`, | ||
| where token comes from `getTokenForAccountQuotaProbe` (same refresh hygiene as Anthropic) and | ||
| projectId from that account's stored credential; missing projectId → throw → existing | ||
| negative-cache/unavailable path (never 0%). | ||
| - Destination: per-account probes go to the registry destination for the account's credential | ||
| (`https://daily-cloudcode-pa.googleapis.com`) only — not `config.baseUrl`. Per-account quota is | ||
| a display of Google's own accounting for that credential; a custom base URL is a routing choice, | ||
| not a second quota source. With a fixed destination the cache key `provider\0accountId` stays | ||
| correct and generation reconciliation keeps working unchanged (blocker 1 gone). Documented. | ||
| - Transport: `providerOutboundPost("google-antigravity", { baseUrl: DAILY }, url, ...)` — the shared | ||
| resolved/pinned transport with `redirect: "manual"` semantics; `providerRedirectError` → null | ||
| quota (blocker 2 gone). The provider-level probe keeps its current behavior (out of scope). | ||
| - Parsing: extract the existing `fetchAvailableModels` → `customWindows` classification into | ||
| `antigravityWindowsFromModels(body)` and reuse it in both paths so Gem/Cla semantics are identical. | ||
| - Route/UI: nothing to change — `/api/oauth/accounts?quota=1` already projects `quota.customWindows` | ||
| through the account list, and the dashboard renders customWindows for Anthropic rows today. | ||
|
|
||
| ## Acceptance | ||
| - Two stored Antigravity accounts → two rows, each probed with its own bearer and its own project id, | ||
| to the fixed Google host; a private/redirecting destination is never given a token (transport test). | ||
| - Missing projectId → unavailable, no request, other account unaffected. | ||
| - Provider-level report unchanged (existing `tests/provider-quota.test.ts` green). | ||
| - `supportsPerAccountQuota("google-antigravity") === true`; unknown/failed never becomes 0%. | ||
| - tsc, privacy, focused: provider-account-quota, provider-quota, oauth-account-routes-related file. | ||
| - Close #2123 with credit for the account loop + token hygiene design and the reasons above. | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| # wp11 audit r1 — synthesis | ||
|
|
||
| Audit input: three reviewer rounds on PR #2123 (Ingwannu), which converged on two structural | ||
| blockers (destination-bound cache identity; pinned outbound transport for every stored bearer). | ||
| The plan removes both by fixing the per-account destination to Google's own host and routing through | ||
| `providerOutboundPost`, so no new cache dimension or reconciliation change is needed. Verdict | ||
| carried as pass for the plan; implementation is verified by the acceptance tests. | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,7 @@ import { resolveProviderApiKey } from "./key-store"; | |
| import { getValidAccessToken, getValidAccessTokenForAccount } from "../oauth"; | ||
| import { getAccountCredential, getAccountSet, getCredential } from "../oauth/store"; | ||
| import { antigravityUserAgent } from "../adapters/client-fingerprint"; | ||
| import { providerOutboundPost, providerRedirectError, type ProviderOutboundDependencies } from "../lib/provider-outbound"; | ||
| import { apiKeyPoolEntryId } from "./api-keys"; | ||
| import { XAI_GROK_CLIENT_VERSION, XAI_GROK_COMPATIBILITY } from "./xai-transport"; | ||
| import { getProviderRegistryEntry, providerCodexAccountMode, registryEntryForProviderDestination } from "./registry"; | ||
|
|
@@ -1474,7 +1475,7 @@ export interface ProviderAccountQuota { | |
|
|
||
| /** Providers whose per-account quota can be probed. Extend as other OAuth APIs are covered. */ | ||
| export function supportsPerAccountQuota(provider: string): boolean { | ||
| return provider === "anthropic" || provider === "kiro"; | ||
| return provider === "anthropic" || provider === "kiro" || provider === "google-antigravity"; | ||
| } | ||
|
|
||
| function accountCacheKey(provider: string, accountId: string): string { | ||
|
|
@@ -1617,7 +1618,16 @@ async function fetchAccountQuota( | |
| quota = kiroSnapshot?.quota ?? null; | ||
| } else { | ||
| const token = await getTokenForAccountQuotaProbe(provider, accountId); | ||
| quota = await fetchAnthropicUsageQuota(token); | ||
| if (provider === "google-antigravity") { | ||
| // Per-account Gem/Cla windows (#1082). The project id is part of the stored | ||
| // credential; without it the probe cannot be made, and that is "unavailable", | ||
| // never 0%. | ||
| const projectId = getAccountCredential(provider, accountId)?.projectId; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If the same Antigravity account is refreshed, reauthenticated, or re-imported after AGENTS.md reference: src/AGENTS.md:L20-L20 Useful? React with 👍 / 👎. |
||
| if (!projectId) throw new Error("antigravity account has no project id"); | ||
| quota = await fetchAntigravityUsageQuota(token, projectId); | ||
| } else { | ||
| quota = await fetchAnthropicUsageQuota(token); | ||
| } | ||
| } | ||
| if (!quota) { | ||
| // Preserve last-good bars and mark unavailable; advance TTL so failures | ||
|
|
@@ -2179,31 +2189,10 @@ function antigravityUsedPercent(quotaInfo: Record<string, unknown>): number | un | |
| return normalizePercent(100 - remaining); | ||
| } | ||
|
|
||
| async function fetchAntigravityQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaReport | null> { | ||
| const credential = getCredential("google-antigravity"); | ||
| if (!credential?.projectId) return null; | ||
| let accessToken: string; | ||
| try { | ||
| accessToken = await getValidAccessToken("google-antigravity"); | ||
| } catch { | ||
| return null; | ||
| } | ||
| const baseUrl = (config.baseUrl || "https://daily-cloudcode-pa.googleapis.com").replace(/\/+$/, ""); | ||
| const response = await fetch(`${baseUrl}/v1internal:fetchAvailableModels`, { | ||
| method: "POST", | ||
| headers: { | ||
| Accept: "application/json", | ||
| "Content-Type": "application/json", | ||
| "User-Agent": antigravityUserAgent(), | ||
| Authorization: `Bearer ${accessToken}`, | ||
| }, | ||
| body: JSON.stringify({ project: credential.projectId }), | ||
| signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), | ||
| }); | ||
| if (!response.ok) return null; | ||
| const body = asRecord(await readQuotaJson(response)); | ||
| /** Gem/Cla windows from a `fetchAvailableModels` body; shared by the provider and account probes. */ | ||
| function antigravityWindowsFromModels(body: Record<string, unknown> | null): ProviderQuotaWindow[] { | ||
| const models = asRecord(body?.models); | ||
| if (!models) return null; | ||
| if (!models) return []; | ||
|
|
||
| const windows = new Map<string, ProviderQuotaWindow>(); | ||
| for (const [modelId, rawModelInfo] of Object.entries(models)) { | ||
|
|
@@ -2226,6 +2215,66 @@ async function fetchAntigravityQuota(provider: string, config: OcxProviderConfig | |
| const window = windows.get(label); | ||
| return window ? [window] : []; | ||
| }); | ||
| return customWindows; | ||
| } | ||
|
|
||
| const ANTIGRAVITY_ACCOUNT_QUOTA_BASE = "https://daily-cloudcode-pa.googleapis.com"; | ||
| let antigravityOutboundDependencies: ProviderOutboundDependencies = {}; | ||
|
|
||
| /** Test seam: inject resolver/pinned transport for the per-account Antigravity probe. */ | ||
| export function setAntigravityAccountQuotaTransportForTests(dependencies: ProviderOutboundDependencies | null): void { | ||
| antigravityOutboundDependencies = dependencies ?? {}; | ||
| } | ||
|
|
||
| /** | ||
| * Per-account Antigravity quota (#1082). Always probes Google's own Cloud Code Assist host | ||
| * through the pinned provider-outbound transport: a configured `baseUrl` is a routing choice | ||
| * for requests, not a second source of Google's accounting for a stored credential, and fixing | ||
| * the destination keeps the `provider\0accountId` cache identity exact across config changes. | ||
| * A redirect or non-2xx yields null (unavailable), never a partial row. | ||
| */ | ||
| export async function fetchAntigravityUsageQuota(accessToken: string, projectId: string): Promise<ProviderQuota | null> { | ||
| const url = `${ANTIGRAVITY_ACCOUNT_QUOTA_BASE}/v1internal:fetchAvailableModels`; | ||
| const response = await providerOutboundPost("google-antigravity", { baseUrl: ANTIGRAVITY_ACCOUNT_QUOTA_BASE }, url, { | ||
| headers: { | ||
| Accept: "application/json", | ||
| "Content-Type": "application/json", | ||
| "User-Agent": antigravityUserAgent(), | ||
| Authorization: `Bearer ${accessToken}`, | ||
| }, | ||
| body: JSON.stringify({ project: projectId }), | ||
| signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), | ||
| }, antigravityOutboundDependencies); | ||
| if (await providerRedirectError(response, url)) return null; | ||
| if (!response.ok) return null; | ||
| const customWindows = antigravityWindowsFromModels(asRecord(await readQuotaJson(response))); | ||
| if (customWindows.length === 0) return null; | ||
| return { customWindows, updatedAt: Date.now() }; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
After a dashboard or Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| async function fetchAntigravityQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaReport | null> { | ||
| const credential = getCredential("google-antigravity"); | ||
| if (!credential?.projectId) return null; | ||
| let accessToken: string; | ||
| try { | ||
| accessToken = await getValidAccessToken("google-antigravity"); | ||
| } catch { | ||
| return null; | ||
| } | ||
| const baseUrl = (config.baseUrl || ANTIGRAVITY_ACCOUNT_QUOTA_BASE).replace(/\/+$/, ""); | ||
| const response = await fetch(`${baseUrl}/v1internal:fetchAvailableModels`, { | ||
| method: "POST", | ||
| headers: { | ||
| Accept: "application/json", | ||
| "Content-Type": "application/json", | ||
| "User-Agent": antigravityUserAgent(), | ||
| Authorization: `Bearer ${accessToken}`, | ||
| }, | ||
| body: JSON.stringify({ project: credential.projectId }), | ||
| signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), | ||
| }); | ||
| if (!response.ok) return null; | ||
| const customWindows = antigravityWindowsFromModels(asRecord(await readQuotaJson(response))); | ||
| if (customWindows.length === 0) return null; | ||
| return report(provider, "google-antigravity:fetchAvailableModels", { | ||
| customWindows, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
HTTP_PROXY/HTTPS_PROXYis configured and this public host does not matchNO_PROXY,providerOutboundRequestdeliberately sends the request throughglobalThis.fetch(src/lib/provider-outbound.ts:170-172) and warns that the final route and peer cannot be pinned locally. Therefore this unconditional user-facing statement is false in a supported environment; document that direct connections are resolved and pinned while proxy-mode connections preserve the configured proxy routing and its weaker peer guarantee.AGENTS.md reference: docs-site/AGENTS.md:L7-L10
Useful? React with 👍 / 👎.