Skip to content

feat(antigravity): preserve per-model quota evidence - #6718

Draft
bi-boo wants to merge 3 commits into
lidge-jun:devfrom
bi-boo:codex/antigravity-model-quota-metadata
Draft

bi-boo wants to merge 3 commits into
lidge-jun:devfrom
bi-boo:codex/antigravity-model-quota-metadata

Conversation

@bi-boo

@bi-boo bi-boo commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Preserve bounded per-model quotaInfo windows from Antigravity's existing fetchAvailableModels fallback. Each diagnostic row carries the exact CCA wire model ID, measured utilization, optional reset time, and the existing quota sample timestamp; per-account rows retain their account association.
  • Keep the current Gem/Cla family summaries unchanged. Account ranking, combo exhaustion, provider routing evidence, and provider urgency sorting ignore model rows. Missing models and failed probes remain unknown; this is diagnostic groundwork and does not implement model permission or eligibility filtering.
  • Bind cached account measurements to the current credential and project identity so a same-ID credential/project replacement cannot reuse prior model data. Carry the optional model ID through management and CLI quota types.
  • The GUI uses its existing generic quota-window renderer; preserving model windows changes the displayed rows even though no new component or layout was added. Related context: [Bug]: Antigravity VALIDATION_REQUIRED 403 does not rotate accounts when web_search is enabled #6666 (this PR is only the model-level evidence prerequisite).

Verification

Current head: 7b297ac0e4d3df190fc2fe890a0d7990200a218f. The follow-up changes only the existing Antigravity retry test expectation to include the exact model diagnostic row. Production and GUI code are unchanged from 63a8c6086fe5c7e17dee9f437ce08b905ca7431c.

  • node_modules/.bin/bun scripts/test.ts tests/providers/provider-antigravity-model-quota.test.ts tests/providers/provider-account-quota.test.ts tests/providers/provider-quota.test.ts tests/providers/provider-antigravity-quota-retry.test.ts — 255 passed, 0 failed, Bun 1.4.0. This fixes three retry assertions discovered by the broader run; the family row and exact gemini model row are both asserted, with the original retry/transport assertions retained.

  • node_modules/.bin/bun run typecheck and node_modules/.bin/bun run privacy:scan — passed after the test correction. git diff --check — passed.

  • Previously completed on the unchanged implementation: GUI model projection 2 passed; focused model-only management routing 1 passed; test-layout guards 18 passed; structure check, GUI build, and docs build passed (existing build warnings remain).

  • node_modules/.bin/bun scripts/test.ts tests/server/management-provider-validation.test.ts — PR implementation 130 passed / 8 failed; independent base bbab02ab270e30219dea97fea07471e776d65479 snapshot 129 passed / the same 8 failed. The new routing-state case passes. Normal approved loopback execution removes the original sandbox Bun.serve({port:0}) failures. A minimal listener fails inside the sandbox and succeeds outside it. The remaining eight return HTTP 400 because local DNS maps opencode.ai to a benchmark-network address and the existing destination policy rejects it. The guard was not disabled; no service/DNS configuration was changed.

  • OCX_TEST_MAIN_TIMEOUT_MS=180000 node_modules/.bin/bun run test:changed — incomplete, exit 124 after 180s, before the retry-assertion correction. Bun selected 1540/2105 files against merge base bbab02ab; the wrapper buffers output until completion. The captured prefix contains 15,354 pass events and 101 fail events, not a completed-suite result. Three failures were the corrected retry expectations; eight were the management failures above. Other failures and timeouts remain; this run is not green.

  • A fresh six-file attribution run on both base and PR head also failed: base 124 passed / 56 failed / 6 errors; head 93 passed / 87 failed / 13 errors (180 tests per run). Command: OCX_TEST_MAIN_TIMEOUT_MS=120000 node_modules/.bin/bun scripts/test.ts tests/adapters/anthropic/anthropic-fast-opt-in.test.ts tests/codex-integration/bearer-admission-routed-provider.test.ts tests/providers/kiro/kiro-model-catalog.test.ts tests/providers/kiro/kiro-model-preference.test.ts tests/claude-integration/claude-models-discovery.test.ts tests/claude-integration/claude-native-passthrough.test.ts. The base diagnostic copy adds response logging only to the local management test helper. Evidence includes the same benchmark-address rejection for api.anthropic.com, mocked Kiro discovery not reaching its expected transport, and timeouts followed by spend-ledger ownership/cleanup errors. The 31 additional head failures were investigated individually in the focused follow-up below; the overall six-file run remains failing. No unrelated runtime changes or validation bypasses were made.

The broad graph touches most of the suite and did not finish within the bounded local run. Full-suite and cross-platform coverage remain outstanding for maintainer-started fork CI. This is a focused-validation scope exception, not a passing full-suite claim. Keep this PR draft with all review-readiness boxes unchecked. Maintainer security review and maintainer-sponsored are still required. No manual external review/CodeRabbit request was made.

Focused attribution follow-up

Aligned unique test names, excluding the duplicated failure summary: all 31 additional head failures are 4 timeouts + 27 spend-ledger ownership conflicts. No further production or test changes were made to the PR head for this investigation. Bun 1.4.0, repository HOME/preload isolation, original 5-second per-test deadlines, and inherited proxy/DNS settings were retained. Fresh runs were sequential using scripts/test.ts --parallel=1 <file> -t <exact names>.

Head-only differences Base evidence Head evidence Attribution
28 bearer-admission cases Exact 28-case subset: 28 pass / 0 fail Same subset: 28 pass / 0 fail Original first case timed out; the next 27 stopped at the unchanged spend-ledger ownership guard before their route assertions. The first case also passes in an independent base/head/head/base sequence.
Kiro preference: capped sibling + explicit proactive off Both individually pass; 2/2 pass with positively populated synthetic caches Same Original fixtures supply pinnedPost only. Under inherited proxy routing, the measured final transport is configuredOutboundFetch, with 0 pinnedPost calls. Empty-cache success alone is not meaningful coverage.
Kiro catalogue: 24-hour expiry Individually passes; 1/1 pass with cached known row verified before advancing time Same Same proxy/mock mismatch; original test can also pass when discovery never populated the cache. Positive-cache comparison verifies the actual expiry assertion.

For the Kiro transport comparison, scratch-only instrumentation mocks the final configuredOutboundFetch seam after the original URL/DNS/security checks, preserving production modules and original assertions. Synthetic 503 replies produce null caches and pass on both versions; valid model replies produce real other/wanted/known cache records and also pass on both. No external transport is emitted by these diagnostic fixtures; no DNS/guard/proxy configuration is disabled.

A controlled fault-injection experiment reproduces the failure mechanism on both versions: delay only the first mocked gateway response by 6000ms, run the first two original bearer cases, and both base/head report 0 pass / 2 fail — first a 5000ms timeout, then the identical SpendLedgerOwnerError for another state directory. Delaying the synthetic Kiro proxy reply by 6000ms similarly yields 0 pass / 1 timeout on both versions; its production discovery timeout is 8000ms, longer than the unchanged test deadline. These intentional failures demonstrate the timeout/cleanup mechanism; they are not passing-suite evidence.

Conclusion and limit: no stable base-pass/head-fail production behavior was reproduced for these 31 differences. The original first bearer timeout's latency source is still unidentified; injected delay proves the cascade exists on base, not why the historical run slowed down. This does not prove all timing regressions impossible, clear the 56 base failures, or replace outstanding controlled-environment CI. No unrelated fix or security-guard bypass was made. PR remains draft.

UI evidence

Actual accountQuotaFromReport + QuotaBars components and the built production CSS from the PR implementation, rendered in an isolated development fixture wrapper. All account labels, percentages, model rows, and reset timestamps are synthetic. No OAuth, real account/token, Google quota request, or paid model call is used by the screenshot harness; external browser requests are blocked. This is component-level evidence, not a live dashboard end-to-end test.

The family-only comparison uses the same renderer with the model rows omitted. The after fixture includes per-account model values at 0%, 22%, 64%, and 100%, plus a missing reset time. Browser checks found 4 family-only rows and 8 rows after, unchanged family urgency (36% / 64%), no page errors, no external requests, and no horizontal overflow at 1280px and 390px. Images were visually inspected.

Screenshots are stored on the fork's existing independent pr-assets branch, pinned to asset commit 89a00155ac4a16a5f0a7722fc0a08cf20fbbb4ab; all four URLs were fetched and their bytes checked against the local PNGs. No evidence image is committed to this PR branch.

Synthetic model quota windows — desktop

Synthetic model quota windows — mobile

Family-only fixture comparison · Compact renderer

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs were updated.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

Review readiness checklist

This PR stays in draft until every box below is ticked. Tick all four boxes once the requirements are met:

  • Required local validation passed; commands, results, and any full-suite exception are documented.

  • I pushed my PR to a recent dev commit (at most 10 behind; a maintainer may still ask for the exact tip before merge).

  • I resolved all correct Codex and CodeRabbit findings.

  • My PR is ready for review.

@coderabbitai

coderabbitai Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the intake: hygiene-blocked Deterministic PR hygiene checks failed label Oct 7, 2026
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

⚠️ Deterministic hygiene checks failed.

  • unsponsored_surface — This changes an authentication, workflow, release-automation, or dependency surface. MAINTAINERS.md requires security review for these; ask a maintainer to apply maintainer-sponsored once they have reviewed it. Paths: src/oauth/account-quota-rank.ts.

@github-actions github-actions Bot added the enhancement New feature or request label Oct 7, 2026
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

⏳ DRAFT

  • hygiene: unsponsored_surface.

What to do

  • Fix unsponsored_surface — This changes an authentication, workflow, release-automation, or dependency surface. MAINTAINERS.md requires security review for these; ask a maintainer to apply maintainer-sponsored once they have reviewed it. Paths: src/oauth/account-quota-rank.ts.
  • Tick all four boxes in the PR description once you're done (currently 0/4).

Review readiness checklist

  • ⬜ Required local validation passed; commands, results, and any full-suite exception are documented.
  • ⬜ I pushed my PR to a recent dev commit (at most 10 behind; a maintainer may still ask for the exact tip before merge).
  • ⬜ I resolved all correct Codex and CodeRabbit findings.
  • ⬜ My PR is ready for review.

0/4 boxes ticked.

This pull request was already a draft. Its draft status will be preserved after every issue above is resolved.
@bi-boo Tick the boxes once required local validation has passed with commands, results, and any full-suite exception documented, your branch is on the latest dev commit, and every correct Codex and CodeRabbit finding is resolved.

bi-boo added a commit to bi-boo/opencodex that referenced this pull request Oct 7, 2026

@Ingwannu Ingwannu left a comment •

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 — The new Antigravity identity fence is bypassed by paused-account publication.

Reviewed exact head 7b297ac0e4d3df190fc2fe890a0d7990200a218f. At src/providers/quota.ts:419, the paused-account shortcut returns before the new currentness check at lines 425–428. accountQuotaProbeSkip() (quota/account-cache.ts:482–485) returns the raw cached entry without checking its isCurrent predicate. Then fetchProviderAccountQuotas() at lines 554–555 propagates that predicate for Anthropic/explicit readers, but not google-antigravity.

Concrete source-level sequence:

  1. Populate this account's model quota measurements under stored identity A.
  2. Replace the stored grant/project in the same account slot with identity B, then pause the account.
  3. getCachedProviderAccountQuota() correctly returns null because A's predicate is false.
  4. A forced account-list quota read takes the pause shortcut, returns A's old measurements, and drops A's predicate. oauth-account-routes.ts:500 therefore cannot reject the row and publishes those stale model bars alongside the current account without quotaUnavailable.

This is an incomplete fence in the claimed cache-identity contract, not a claim that every login endpoint reproduces the sequence or that this PR introduced all pre-existing pause behavior. Please preserve the no-network pause behavior while validating retained measurements against the current identity. Either normalize an invalid paused entry to unavailable/null before returning it, or retain its currentness predicate through the result so management publication rejects it. Keep same-identity paused last-good retention intact; do not clear all paused measurements as a workaround.

I prepared two reviewer-only cases in the existing provider-account-quota harness: unchanged paused identity retains the seeded exact model row, replaced paused identity must not publish it, and neither pause read emits another transport. They allow either null/unavailable or a false propagated predicate. They have NOT been executed: foreign Cargo/rustc/clippy workloads are still running on the shared host. The finding is the concrete source path above, not a claimed red test or live account/provider reproduction. Production files and the contributor branch were not changed. No security/privacy scan was run.

The existing current-head gate annotations say unsponsored_surface; this review does not grant sponsorship, independent boundary signoff, CI success, or approval.

Measured verification update — 2026-10-11

The unexecuted-fixture paragraph above describes the initial review. I have now run those two reviewer-only cases on the unchanged production head 7b297ac0e4d3df190fc2fe890a0d7990200a218f:

Bun 1.4.0 test --no-env-file --isolate ./tests/providers/provider-account-quota.test.ts --test-name-pattern 'review-only paused model quota' --timeout 15000

1 pass / 1 fail / 64 filtered / 11 assertions. The same-identity paused control passes. In the replaced-identity case, the synchronous cached reader first correctly returns null; the forced paused account read then returns the old Gem/model 24% quota object with no false publication predicate, failing the effective-publication assertion. Both pause reads make zero additional synthetic transport calls. This independently reproduces the scoped correctness defect; no real provider/account/SSH/native service was contacted.

The run waited for foreign builds to clear before starting Bun, used fresh private HOME/CODEX_HOME/OPENCODEX_HOME/TMPDIR, and verified effective CPU 75%, memory high 1280MiB/max1536MiB, swap0, tasks64, nice10, plus a finite90-second unit ceiling. IOWeight20 was requested but the user's cgroup lacks a delegated io controller, so no effective I/O-weight enforcement is claimed. Protected user configuration is unchanged; the owned unit/PID/cgroup are terminal/removed. Two earlier launcher checks exited before Bun and are not counted as test runs. No whole-suite/typecheck/platform pass or approval is implied; this change request remains active.

Reviewer-local correction candidate — 2026-10-11

I have now implemented a narrow follow-up in a separate owned local branch atop this unchanged PR head. The contributor branch is untouched; the change request remains active until a fix actually reaches it. Local-only commit 339215d17c62d20e72656f556a0ed57d337127b9 is not a published GitHub commit.

The candidate makes the paused-cache shortcut honor an existing isCurrent fence and carries Antigravity's private callback through quota-row projection. Matching paused measurements remain available with zero additional sends; grant-only, project-only and combined replacement become unavailable. A returned matching row also becomes noncurrent after a later replacement, allowing the management consumer's existing final check to reject it. The callback remains nonenumerable. The existing projection moves into quota/account-cache.ts rather than increasing the quota facade's cap: src/providers/quota.ts is550 lines against the unchanged558 cap (this PR head was564). The provider structure contract is updated; no dependency, new setting or routing policy is introduced.

Measured regression on the unchanged production code before the correction: 0 pass / 4 fail / 64 filtered / 25 assertions for bun test --no-env-file --isolate ./tests/providers/provider-account-quota.test.ts --test-name-pattern 'paused model quota retains only current' --timeout 15000. Three replacements publish the old24% model quota; the unchanged control lacks the publication callback.

After the correction, the complete existing provider-account-quota file including those four cases passed: 68 pass / 0 fail / 422 assertions, Bun1.4.0/Linux, command bun test --no-env-file --isolate ./tests/providers/provider-account-quota.test.ts --timeout 15000. Existing Anthropic and explicit-provider projection/refresh/caching cases are included. bun --no-env-file scripts/structure-ssot.ts passed, and bun test --no-env-file --isolate ./tests/ci-workflows/file-size-ratchet.test.ts --timeout 15000 passed 9 tests / 85 assertions at that source/document checkpoint. No baseline was raised. Test transports, credentials, DNS and refresh responses are synthetic, with fresh private homes, effective CPU75%/memoryHigh1280MiB/max1536MiB/swap0/tasks64/nice10; small gates used CPU50%/384MiB-high/512MiB-max/tasks32. IOWeight20 has no delegated-kernel-I/O enforcement claim.

The patch also includes two direct management-route fixtures. They are prepared but not executed: the subsequent serial admission window ended after60 seconds with foreign builds still live, before Bun started. Root typecheck, the full repository suite, final expanded API-file verification and cross-platform qualification remain pending. I am not treating the local candidate as review-ready or as an author fix; no scan, real account/provider/daemon call, CI authorization, sponsorship or approval occurred. The source/doc gate receipt predates only the additional API-test edits, not a later runtime change.

For a directly reviewable/applicable follow-up, the complete five-file patch against 7b297ac0e4d3df190fc2fe890a0d7990200a218f is below. It is a proposal, not a claim that the PR branch now contains it.

diff --git a/src/providers/quota.ts b/src/providers/quota.ts
index 62603cb508..0d8bda6dcc 100644
--- a/src/providers/quota.ts
+++ b/src/providers/quota.ts
@@ -50,6 +50,7 @@ import {
   mayCommitAccountQuotaKey,
   mayCommitProviderQuotaKey,
   normalizeAnthropicQuota,
+  projectProviderAccountQuota,
   supportsPerAccountQuota,
   type AccountQuotaCacheEntry,
   type ProviderAccountQuota,
@@ -544,21 +545,6 @@ export async function fetchProviderAccountQuotas(
   if (!set) return [];
   return mapQuotaRoster(set.accounts, async account => {
     const entry = await fetchAccountQuota(provider, account.id, forceRefresh, providerConfig);
-    const result: ProviderAccountQuota = {
-      accountId: account.id,
-      quota: provider === "anthropic" ? normalizeAnthropicQuota(entry.quota, Date.now()) : entry.quota,
-      ...(entry.unavailable ? { unavailable: true as const } : {}),
-      ...(entry.unavailable && entry.quotaFailure && entry.quotaFailureIsCurrent?.() === true ? { quotaFailure: entry.quotaFailure } : {}),
-    };
-    if (entry.quotaFailureIsCurrent) Object.defineProperty(result, "quotaFailureIsCurrent", { value: entry.quotaFailureIsCurrent });
-    if (provider === "anthropic" && entry.isCurrent) Object.defineProperty(result, "isCurrent", { value: entry.isCurrent });
-    if (!explicitAccountReader(provider)) return result;
-    const identity = entry.identity;
-    Object.defineProperty(result, "isCurrent", { value: () => {
-      if (entry.isCurrent) return entry.isCurrent();
-      const credential = getAccountCredential(provider, account.id);
-      return !!credential && (!identity || explicitQuotaIdentity(provider, account.id, providerConfig) === identity);
-    } });
-    return result;
+    return projectProviderAccountQuota(provider, account.id, entry, providerConfig);
   });
 }
diff --git a/src/providers/quota/account-cache.ts b/src/providers/quota/account-cache.ts
index 879ca013d8..ea2a641f7f 100644
--- a/src/providers/quota/account-cache.ts
+++ b/src/providers/quota/account-cache.ts
@@ -180,6 +180,29 @@ export interface ProviderAccountQuota {
   isCurrent?: () => boolean;
 }
 
+/** Keep private identity fences attached until the management consumer projects the row. */
+export function projectProviderAccountQuota(
+  provider: string, accountId: string, entry: AccountQuotaCacheEntry, providerConfig?: OcxProviderConfig,
+): ProviderAccountQuota {
+  const result: ProviderAccountQuota = {
+    accountId,
+    quota: provider === "anthropic" ? normalizeAnthropicQuota(entry.quota, Date.now()) : entry.quota,
+    ...(entry.unavailable ? { unavailable: true as const } : {}),
+    ...(entry.unavailable && entry.quotaFailure && entry.quotaFailureIsCurrent?.() === true ? { quotaFailure: entry.quotaFailure } : {}),
+  };
+  if (entry.quotaFailureIsCurrent) Object.defineProperty(result, "quotaFailureIsCurrent", { value: entry.quotaFailureIsCurrent });
+  if ((provider === "anthropic" || provider === "google-antigravity") && entry.isCurrent)
+    Object.defineProperty(result, "isCurrent", { value: entry.isCurrent });
+  if (!explicitAccountReader(provider)) return result;
+  const identity = entry.identity;
+  Object.defineProperty(result, "isCurrent", { value: () => {
+    if (entry.isCurrent) return entry.isCurrent();
+    const credential = getAccountCredential(provider, accountId);
+    return !!credential && (!identity || explicitQuotaIdentity(provider, accountId, providerConfig) === identity);
+  } });
+  return result;
+}
+
 /** 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" || provider === "google-antigravity"
@@ -476,13 +499,15 @@ export async function getTokenForAccountQuotaProbe(provider: string, accountId:
 
 /**
  * Rows that must not be probed: an unsupported provider reads as unavailable, and a paused
- * account returns its last reading unchanged (its `ts` shows the age) or an unavailable row,
+ * account returns its current-identity last reading (its `ts` shows the age) or an unavailable row,
  * without writing the cache. Undefined means "probe normally".
  */
 export function accountQuotaProbeSkip(provider: string, accountId: string): AccountQuotaCacheEntry | undefined {
   if (!supportsPerAccountQuota(provider)) return { ts: Date.now(), quota: null, unavailable: true };
   if (getAccountCredentialWithStatus(provider, accountId)?.paused !== true) return undefined;
-  return accountQuotaCache.get(accountCacheKey(provider, accountId)) ?? { ts: Date.now(), quota: null, unavailable: true };
+  const cached = accountQuotaCache.get(accountCacheKey(provider, accountId));
+  // Pause suppresses network work, not the ownership fence used by ordinary cached reads.
+  return (cached?.isCurrent?.() !== false ? cached : undefined) ?? { ts: Date.now(), quota: null, unavailable: true };
 }
 
 export function explicitQuotaConfig(provider: string, configured?: OcxProviderConfig): OcxProviderConfig | undefined {
diff --git a/structure/providers-and-adapters.md b/structure/providers-and-adapters.md
index 852afabdbc..9ee82e5648 100644
--- a/structure/providers-and-adapters.md
+++ b/structure/providers-and-adapters.md
@@ -141,7 +141,7 @@ account-list exclusion reason; cached plan credit amounts share the same identit
 expiry fence.
 Across generic OAuth providers, pause also excludes that account from Token Guardian's
 proactive refresh, per-account quota probes (`accountQuotaProbeSkip` in
-`src/providers/quota/account-cache.ts` returns the last reading without a request), the Meta
+`src/providers/quota/account-cache.ts` returns only an identity-current last reading without a request), the Meta
 Muse key-mint quota read, and xAI/Gemini web-search sidecar eligibility. The stored credential
 remains available for resume, while requests with no unpaused account fail with 403 rather than
 as a login failure.
@@ -579,7 +579,7 @@ Antigravity catalog rows carry the observed complete effort map from `src/provid
 
 Known Claude Sonnet and Opus 5.5 base and low/medium/high IDs share a deterministic usage identity in `src/providers/antigravity-models.ts`, independently of live discovery. This accounting map does not migrate saved routing selections; Claude 4.6 and unknown suffix identities remain unchanged.
 
-When the `retrieveUserQuotaSummary` endpoint has no usable result, `src/providers/quota/antigravity.ts` preserves bounded per-model `quotaInfo` measurements from `fetchAvailableModels` alongside the existing first-seen Gem/Cla family summaries. Model windows carry the exact CCA wire ID for display and diagnostics; account and provider routing ignore these rows and continue to use family summaries. A missing model row or failed probe is unknown evidence, not a model eligibility verdict. Per-account cache entries are bound to the current credential and project identity.
+When the `retrieveUserQuotaSummary` endpoint has no usable result, `src/providers/quota/antigravity.ts` preserves bounded per-model `quotaInfo` measurements from `fetchAvailableModels` alongside the existing first-seen Gem/Cla family summaries. Model windows carry the exact CCA wire ID for display and diagnostics; account and provider routing ignore these rows and continue to use family summaries. A missing model row or failed probe is unknown evidence, not a model eligibility verdict. Per-account cache entries are bound to the current credential and project identity. Paused reads retain matching measurements without a send, but return unavailable after grant or project replacement. `projectProviderAccountQuota` in `src/providers/quota/account-cache.ts` preserves the private, nonserialized currentness callback for management to recheck after roster awaits; pause cannot revive retired measurements.
 
 
 ## Combo model refusal evidence
diff --git a/tests/providers/provider-account-quota.test.ts b/tests/providers/provider-account-quota.test.ts
index 7760bb6a31..3a28b9b045 100644
--- a/tests/providers/provider-account-quota.test.ts
+++ b/tests/providers/provider-account-quota.test.ts
@@ -920,6 +920,54 @@ describe("google-antigravity per-account quota (#1082)", () => {
     expect(getCachedProviderAccountQuota("google-antigravity", accountId)).toBeNull();
   });
 
+  test.each(["unchanged", "grant", "project", "both"] as const)("paused model quota retains only current credential and project (%s)", async replacement => {
+    const { setAccountPaused } = await import("../../src/oauth/store");
+    const credential = { access: "paused-model-first", refresh: "paused-refresh", expires: Date.now() + 3600_000,
+      projectId: "paused-project-first", accountId: "paused-model-user", email: "paused-model@example.com" };
+    await saveCredential("google-antigravity", credential);
+    let posts = 0;
+    globalThis.fetch = (async () => { throw new Error("unexpected external quota transport"); }) as typeof fetch;
+    setAntigravityAccountQuotaTransportForTests({
+      resolveAddresses: async () => ({ hostname: "daily-cloudcode-pa.googleapis.com",
+        addresses: [{ address: "142.250.0.1", family: 4 }], privateNetwork: false }),
+      pinnedPost: async url => {
+        posts++;
+        return url.endsWith("retrieveUserQuotaSummary") ? new Response(null, { status: 404 })
+          : Response.json({ models: { "gemini-paused-model": { quotaInfo: { remainingFraction: 0.76 } } } });
+      },
+    });
+    const [seeded] = await fetchProviderAccountQuotas("google-antigravity", true);
+    const accountId = idFor("paused-model@example.com");
+    expect(seeded?.quota?.customWindows).toContainEqual({
+      label: "gemini-paused-model", modelId: "gemini-paused-model", percent: 24,
+    });
+    expect(posts).toBe(2);
+    if (replacement !== "unchanged") {
+      await saveCredential("google-antigravity", { ...credential,
+        ...(replacement !== "project" ? { access: "paused-model-second" } : {}),
+        ...(replacement !== "grant" ? { projectId: "paused-project-second" } : {}),
+      });
+      expect(idFor("paused-model@example.com")).toBe(accountId);
+    }
+    await setAccountPaused("google-antigravity", accountId, true);
+    const expected = replacement === "unchanged" ? seeded!.quota : null;
+    expect(getCachedProviderAccountQuota("google-antigravity", accountId)).toEqual(expected);
+    const [paused] = await fetchProviderAccountQuotas("google-antigravity", true);
+    expect(posts).toBe(2);
+    expect(paused?.quota).toEqual(expected);
+    if (replacement !== "unchanged") expect(paused?.unavailable).toBe(true);
+    else {
+      expect(paused?.unavailable).toBeUndefined();
+      expect(paused?.isCurrent?.()).toBe(true);
+      expect(Object.keys(paused!)).not.toContain("isCurrent");
+      // A sibling roster read may await before management projects this already-returned row.
+      await saveCredential("google-antigravity", { ...credential, access: "paused-model-later" });
+      expect(paused?.isCurrent?.()).toBe(false);
+      expect(getCachedProviderAccountQuota("google-antigravity", accountId)).toBeNull();
+      expect(posts).toBe(2);
+    }
+  });
+
   test("NO_PROXY denial preserves an unavailable account row without sending its bearer", async () => {
     await saveCredential("google-antigravity", { access: "agy-first", refresh: "r1", expires: Date.now() + 3600_000, projectId: "proj-first", accountId: "agy-a", email: "a@example.com" });
     process.env.no_proxy = "daily-cloudcode-pa.googleapis.com";
diff --git a/tests/server/provider-account-quota-routes.test.ts b/tests/server/provider-account-quota-routes.test.ts
index f6bf9a2329..80edc682aa 100644
--- a/tests/server/provider-account-quota-routes.test.ts
+++ b/tests/server/provider-account-quota-routes.test.ts
@@ -21,6 +21,7 @@ beforeEach(() => {
 });
 afterEach(() => {
   globalThis.fetch = originalFetch;
+  quotaApi.setAntigravityAccountQuotaTransportForTests(null);
   clearAccountQuotaCache();
   clearProviderQuotaCache();
   if (originalHome === undefined) delete process.env.OPENCODEX_HOME;
@@ -50,6 +51,40 @@ async function read(path: string, config: OcxConfig) {
   return response;
 }
 
+test.each([false, true])("paused Antigravity account API publishes only current model quota (replaced=%s)", async replaced => {
+  const { setAccountPaused } = await import("../../src/oauth/store");
+  const credential = { access: "paused-api-first", refresh: "paused-api-refresh", expires: Date.now() + 3600_000,
+    projectId: "paused-api-project-first", accountId: "paused-api-user", email: "paused-api@example.test" };
+  await saveCredential("google-antigravity", credential);
+  const config: OcxConfig = { port: 0, defaultProvider: "google-antigravity", providers: {
+    "google-antigravity": { adapter: "google", authMode: "oauth", baseUrl: "https://daily-cloudcode-pa.googleapis.com" },
+  } };
+  let posts = 0;
+  globalThis.fetch = (async () => { throw new Error("unexpected external quota transport"); }) as typeof fetch;
+  quotaApi.setAntigravityAccountQuotaTransportForTests({
+    resolveAddresses: async () => ({ hostname: "daily-cloudcode-pa.googleapis.com",
+      addresses: [{ address: "142.250.0.1", family: 4 }], privateNetwork: false }),
+    pinnedPost: async url => {
+      posts++;
+      return url.endsWith("retrieveUserQuotaSummary") ? new Response(null, { status: 404 })
+        : Response.json({ models: { "gemini-paused-api-model": { quotaInfo: { remainingFraction: 0.76 } } } });
+    },
+  });
+  const [seeded] = await quotaApi.fetchProviderAccountQuotas("google-antigravity", true);
+  const accountId = getAccountSet("google-antigravity")!.accounts[0]!.id;
+  expect(seeded?.quota?.customWindows).toContainEqual({ label: "gemini-paused-api-model", modelId: "gemini-paused-api-model", percent: 24 });
+  expect(posts).toBe(2);
+  if (replaced) await saveCredential("google-antigravity", { ...credential, access: "paused-api-second", projectId: "paused-api-project-second" });
+  await setAccountPaused("google-antigravity", accountId, true);
+  const body = await (await read("/api/oauth/accounts?provider=google-antigravity&quota=1&refresh=1", config)).json();
+  expect(body.accounts).toHaveLength(1);
+  expect(body.accounts[0].id).toBe(accountId);
+  expect(body.accounts[0].quota).toEqual(replaced ? null : seeded!.quota);
+  expect(body.accounts[0].quotaUnavailable).toBe(replaced);
+  expect(posts).toBe(2);
+  expect(JSON.stringify(body)).not.toContain("isCurrent");
+});
+
 test("cheap and refresh-only key lists advertise capability without probing or changing config", async () => {
   const config = keyConfig();
   const before = JSON.stringify(config);

Management API verification update — s109, 2026-10-11

The two previously unexecuted API cases have now run as part of the complete management route file on reviewer-local commit 339215d17c62d20e72656f556a0ed57d337127b9 (author head unchanged). Command: bun test --no-env-file --isolate ./tests/server/provider-account-quota-routes.test.ts --timeout 15000, Bun1.4.0/Linux: 12 pass / 0 fail / 59 assertions,1.158 seconds. This directly exercises handleOauthAccountRoutes: a matching paused identity retains its seeded model quota, while the replaced paused identity returns quota: null and quotaUnavailable: true, with no further transport sends. The private callback is not serialized. These are disposable private auth-store/DNS/pinned-transport fixtures and in-process GET handling, not live server/provider/account calls or real configuration persistence.

One bounded root typecheck attempt then used the existing TypeScript7.0.2 compiler directly (bun --no-env-file node_modules/typescript/bin/tsc --noEmit, no download/hooks). It did not complete: the180-second unit deadline plus5-second termination grace ended it with timeout/KILL (185.106 seconds,83.304 seconds CPU, reported peak~1.2GiB,swap0). MemoryHigh throttling was observed; no completed compiler diagnostic/pass is available. Caps were not raised and no retry was made. The owned cgroup is removed. Root tsconfig includes src only, so new test strict typing would not be proved even by a root pass.

Thus the local candidate now has the68-case provider file and12-case API file passing, but typecheck/full-suite/cross-platform/readiness remain incomplete. The author's PR has not received the patch, and this change request is not withdrawn.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request intake: hygiene-blocked Deterministic PR hygiene checks failed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants