Skip to content
Closed
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
4 changes: 4 additions & 0 deletions docs-site/src/content/docs/reference/management-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,10 @@ setting interacts with `claudeCode.enabled` across upgrades and downgrades.
| `GET, PUT /api/provider-context-caps` | Read or update global, all-provider, or one-provider context caps | 400 invalid request; 404 unknown provider |
| `GET /api/provider-presets` | Return GUI provider presets derived from the runtime registry | — |

When Antigravity's quota-summary endpoint has no usable response, quota reports may also include
per-model Cloud Code Assist usage windows when the model-discovery response provides them. These
measurements are diagnostic; an absent model window does not establish that the model is unavailable.

The provider context-cap response includes `caps` (active limits) and `values` (last selected
values, retained while disabled). Enabling a provider without `value` restores its selection,
or uses the global `contextCapValue` on first enable. This also applies to OpenAI: the switch
Expand Down
2 changes: 1 addition & 1 deletion gui/src/codex-quota-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export interface AccountQuota {
shortObservedAt?: number;
shortWindowSeconds?: number;
monthlyResetAt?: number;
customWindows?: { label: string; percent: number; resetAt?: number }[];
customWindows?: { label: string; percent: number; resetAt?: number; modelId?: string }[];
creditsUsd?: {
used: number;
limit: number;
Expand Down
1 change: 1 addition & 0 deletions gui/src/components/QuotaBars.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ export function maxQuotaUtilisation(quota: AccountQuota | null): number {
const vals = [quota.fiveHourPercent, quota.weeklyPercent, quota.monthlyPercent]
.filter((n): n is number => typeof n === "number");
for (const w of quota.customWindows ?? []) {
if (w.modelId !== undefined) continue;
if (typeof w.percent === "number") vals.push(w.percent);
}
const hasSubscriptionCreditsCustom = quota.customWindows?.some(
Expand Down
1 change: 1 addition & 0 deletions gui/src/provider-workspace/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ function quotaFromUnknown(quota: unknown, fallbackUpdatedAt?: number): AccountQu
label: row.label,
percent: row.percent as number,
...(finite(row.resetAt) !== undefined ? { resetAt: row.resetAt as number } : {}),
...(typeof row.modelId === "string" && row.modelId.length <= 128 ? { modelId: row.modelId } : {}),
}];
})
: [];
Expand Down
29 changes: 29 additions & 0 deletions gui/tests/provider-quota-model-id.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { expect, test } from "bun:test";
import { accountQuotaFromReport } from "../src/provider-workspace/report";
import { maxQuotaUtilisation } from "../src/components/QuotaBars";

test("provider quota projection preserves bounded exact Antigravity model IDs", () => {
expect(accountQuotaFromReport({
updatedAt: 123,
quota: {
updatedAt: 123,
customWindows: [
{ label: "gemini-3.8-flash-high", modelId: "gemini-3.8-flash-high", percent: 24 },
{ label: "legacy", modelId: "x".repeat(129), percent: 12 },
],
},
})?.customWindows).toEqual([
{ label: "gemini-3.8-flash-high", modelId: "gemini-3.8-flash-high", percent: 24 },
{ label: "legacy", percent: 12 },
]);
});

test("model diagnostic bars do not change provider urgency sorting", () => {
expect(maxQuotaUtilisation({
updatedAt: 123,
customWindows: [
{ label: "Gem", percent: 36 },
{ label: "gemini-3.8-flash-high", modelId: "gemini-3.8-flash-high", percent: 100 },
],
})).toBe(36);
});
1 change: 1 addition & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"gui-pair-delivery.test.ts": "gui",
"usage-antigravity-55.test.ts": "usage",
"provider-antigravity-wire-snapshot.test.ts": "providers",
"provider-antigravity-model-quota.test.ts": "providers",
"antigravity-discovered-families.test.ts": "adapters/google",
"provider-antigravity-effort-families.test.ts": "providers",
"provider-antigravity-family-catalog.test.ts": "providers",
Expand Down
1 change: 1 addition & 0 deletions src/cli/account-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ export interface ProviderQuotaWindowDto {
label: string;
percent: number;
resetAt?: number;
modelId?: string;
}

export interface ProviderQuotaDto extends CodexQuotaDto {
Expand Down
3 changes: 3 additions & 0 deletions src/combos/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ function quotaWindowExhausted(percent: number | undefined, resetAt: number | und
const MODEL_FAMILY_WINDOW_LABELS = new Set(["fable", "opus", "sonnet"]);

function customWindowAppliesToModel(window: ProviderQuotaWindow, model: string | undefined): boolean {
// Exact Antigravity model readings are diagnostic only. They are not permission or
// provider-wide exhaustion evidence until a separate eligibility contract consumes them.
if (window.modelId !== undefined) return false;
if (window.scope !== "model" || model === undefined) return true;
const family = window.label.trim().toLowerCase();
if (!MODEL_FAMILY_WINDOW_LABELS.has(family)) return true;
Expand Down
4 changes: 2 additions & 2 deletions src/oauth/account-quota-rank.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ function headroomOf(provider: string, accountId: string, requestedModelId?: stri
const family = classifyModelFamilyForQuota(provider, requestedModelId);
if (family) {
const percents = (quota.customWindows ?? [])
.filter(window => windowMatchesFamily(window.label, family))
.filter(window => window.modelId === undefined && windowMatchesFamily(window.label, family))
.map(window => window.percent)
.filter((value): value is number => typeof value === "number");
if (percents.length === 0) return null;
Expand All @@ -106,7 +106,7 @@ function headroomOf(provider: string, accountId: string, requestedModelId?: stri
quota.fiveHourPercent,
quota.weeklyPercent,
quota.monthlyPercent,
...(quota.customWindows ?? []).map(window => window.percent),
...(quota.customWindows ?? []).filter(window => window.modelId === undefined).map(window => window.percent),
].filter((value): value is number => typeof value === "number");
if (percents.length === 0) return null;
return 100 - Math.max(...percents);
Expand Down
2 changes: 2 additions & 0 deletions src/providers/quota-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export interface ProviderQuotaWindow {
label: string;
percent: number;
resetAt?: number;
/** Exact CCA wire model measured by this window; display evidence only, not eligibility. */
modelId?: string;
/**
* Set only when the PRODUCER proved this window covers one model family, structurally rather
* than by reading its label: an Anthropic `seven_day_<family>` body key, or a limit with
Expand Down
11 changes: 9 additions & 2 deletions src/providers/quota.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,8 @@ async function fetchAccountQuota(
const kiroIdentity = provider === "kiro" ? kiroProbeIdentity(accountId) : undefined;
const cachedCandidate = accountQuotaCache.get(key);
const cached = (provider !== "kiro" || cachedCandidate?.identity === kiroIdentity)
&& (provider !== "anthropic" || cachedCandidate?.isCurrent?.() !== false) ? cachedCandidate : undefined;
&& (provider !== "anthropic" || cachedCandidate?.isCurrent?.() !== false)
&& (provider !== "google-antigravity" || cachedCandidate?.isCurrent?.() !== false) ? cachedCandidate : undefined;
if (!forceRefresh && cached && Date.now() - cached.ts < ACCOUNT_QUOTA_TTL_MS) {
if (provider === "google-antigravity" && cached.quotaFailure && cached.quotaFailureIsCurrent?.() !== true) return { ...cached, quotaFailure: undefined };
return provider === "anthropic" ? { ...cached, quota: normalizeAnthropicQuota(cached.quota, Date.now()) } : cached;
Expand All @@ -447,6 +448,7 @@ async function fetchAccountQuota(
try { return epoch === explicitAccountEpoch && diagnosticIdentity !== undefined && diagnosticIdentity === antigravityQuotaDiagnosticIdentity(accountId); }
catch { return false; }
};
const antigravityIsCurrent = () => quotaFailureIsCurrent();
const diagnosticFields = () => quotaFailure && quotaFailureIsCurrent() ? { quotaFailure, quotaFailureIsCurrent } : {};
const unavailable = (): AccountQuotaCacheEntry => {
if (!flightCurrent()) return { ts: Date.now(), quota: null, unavailable: true };
Expand All @@ -458,9 +460,11 @@ async function fetchAccountQuota(
unavailable: true,
...(provider === "anthropic" && retained?.isCurrent ? { isCurrent: retained.isCurrent } : {}),
...(provider === "kiro" ? { identity: kiroIdentity } : {}),
...(provider === "google-antigravity" ? { isCurrent: antigravityIsCurrent } : {}),
...diagnosticFields(),
};
if (mayCommitAccountQuotaKey(key, writerGeneration) && (provider !== "kiro" || kiroProbeCurrent(accountId, kiroIdentity))) {
if (entry.isCurrent?.() !== false && mayCommitAccountQuotaKey(key, writerGeneration)
&& (provider !== "kiro" || kiroProbeCurrent(accountId, kiroIdentity))) {
accountQuotaCache.set(key, entry);
if (provider === "kiro") persistKiroAccountState();
sweepExpiredOnWrite(entry.ts);
Expand Down Expand Up @@ -499,13 +503,16 @@ async function fetchAccountQuota(
return { ts: Date.now(), quota: null, unavailable: true };
}
}
if (provider === "google-antigravity" && !antigravityIsCurrent()) return { ts: Date.now(), quota: null, unavailable: true };
if (!quota) return unavailable();
const entry: AccountQuotaCacheEntry = {
ts: Date.now(), quota: provider === "anthropic" ? normalizeAnthropicQuota(quota, Date.now()) : quota,
...(provider === "kiro" ? { identity: kiroIdentity } : {}),
...(provider === "google-antigravity" ? { isCurrent: antigravityIsCurrent } : {}),
...(anthropicCurrent ? { isCurrent: anthropicCurrent } : {}),
};
if (mayCommitAccountQuotaKey(key, writerGeneration)
&& entry.isCurrent?.() !== false
&& (provider !== "kiro" || kiroProbeCurrent(accountId, kiroIdentity))) {
accountQuotaCache.set(key, entry);
if (provider === "kiro") { commitKiroAccountUsageState(key, kiroSnapshot, kiroIdentity); persistKiroAccountState(); }
Expand Down
29 changes: 22 additions & 7 deletions src/providers/quota/antigravity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,27 +71,42 @@ function antigravityWindowsFromModels(body: Record<string, unknown> | null): Pro
if (!models) return [];

const windows = new Map<string, ProviderQuotaWindow>();
const modelWindows: ProviderQuotaWindow[] = [];
for (const [modelId, rawModelInfo] of Object.entries(models)) {
if (!modelId.trim() || modelId.length > 128) continue;
const modelInfo = asRecord(rawModelInfo);
if (!modelInfo) continue;
for (const quotaInfo of quotaInfoEntries(modelInfo)) {
const label = classifyAntigravityFamily(modelId, modelInfo, quotaInfo);
if (!label || windows.has(label)) continue;
const percent = antigravityUsedPercent(quotaInfo);
if (percent === undefined) continue;
windows.set(label, {
label,
percent,
...(normalizeResetAt(quotaInfo.resetTime) !== undefined ? { resetAt: normalizeResetAt(quotaInfo.resetTime) } : {}),
});
const resetAt = normalizeResetAt(quotaInfo.resetTime);
if (label && !windows.has(label)) {
windows.set(label, { label, percent, ...(resetAt !== undefined ? { resetAt } : {}) });
}
// Keep model measurements as display/diagnostic evidence. They are deliberately
// separate from the family summaries above; routing must not infer eligibility
// from one model's counter or from a missing row.
if (modelWindows.length < 256) {
const tier = typeof quotaInfo.tier === "string" && quotaInfo.tier.length <= 64
? quotaInfo.tier.trim().slice(0, Math.max(0, 125 - modelId.length))
: "";
const modelLabel = `${modelId}${tier ? ` · ${tier}` : ""}`;
modelWindows.push({
label: modelLabel,
modelId,
percent,
...(resetAt !== undefined ? { resetAt } : {}),
});
}
}
}

const customWindows = ["Gem", "Cla"].flatMap(label => {
const window = windows.get(label);
return window ? [window] : [];
});
return customWindows;
return [...customWindows, ...modelWindows];
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/server/management/provider-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -872,7 +872,7 @@ function providerRoutingQuota(config: OcxConfig, name: string, now: number): Pro

// Removing search/MCP windows may leave only a timestamp. That is not inference evidence.
const percentages = [quota.fiveHourPercent, quota.weeklyPercent, quota.monthlyPercent,
...(quota.customWindows ?? []).map(window => window.percent)];
...(quota.customWindows ?? []).filter(window => window.modelId === undefined).map(window => window.percent)];
const hasPercentage = percentages.some(value => typeof value === "number" && Number.isFinite(value) && value >= 0);
const credits = quota.creditsUsd;
const hasCredits = credits !== undefined && Number.isFinite(credits.percent)
Expand All @@ -883,7 +883,7 @@ function providerRoutingQuota(config: OcxConfig, name: string, now: number): Pro
let validUntil = quota.updatedAt + PROVIDER_QUOTA_MAX_AGE_MS;
if (state === "exhausted") {
const resets = [quota.fiveHourResetAt, quota.weeklyResetAt, quota.monthlyResetAt,
...(quota.customWindows ?? []).map(window => window.resetAt)]
...(quota.customWindows ?? []).filter(window => window.modelId === undefined).map(window => window.resetAt)]
.filter((reset): reset is number => typeof reset === "number" && Number.isFinite(reset)
&& reset > now && reset < validUntil)
.sort((left, right) => left - right);
Expand Down
2 changes: 2 additions & 0 deletions structure/providers-and-adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,8 @@ 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.


## Combo model refusal evidence

Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/test-layout-expected.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"ollama-native-tool-continuations.test.ts": "providers/ollama",
"gui-pair-delivery.test.ts": "gui",
"usage-antigravity-55.test.ts": "usage", "provider-antigravity-wire-snapshot.test.ts": "providers",
"provider-antigravity-model-quota.test.ts": "providers",
"antigravity-discovered-families.test.ts": "adapters/google",
"provider-antigravity-effort-families.test.ts": "providers",
"provider-antigravity-family-catalog.test.ts": "providers",
Expand Down
36 changes: 30 additions & 6 deletions tests/providers/provider-account-quota.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -735,7 +735,7 @@ describe("google-antigravity per-account quota (#1082)", () => {
const byId = Object.fromEntries(rows.map(row => [row.accountId, row]));
const [idA, idB] = [idFor("a@example.com"), idFor("b@example.com")];
expect(Object.keys(byId).sort()).toEqual([idA, idB].sort());
const windows = (id: string) => byId[id]!.quota!.customWindows!.map(w => `${w.label}=${w.percent}`);
const windows = (id: string) => byId[id]!.quota!.customWindows!.filter(w => w.modelId === undefined).map(w => `${w.label}=${w.percent}`);
expect(windows(idA)).toEqual(["Gem=14", "Gem (Weekly)=14", "Cla=62", "Cla (Weekly)=62"]);
expect(windows(idB)).toEqual(["Gem=3", "Gem (Weekly)=3", "Cla=9", "Cla (Weekly)=9"]);
expect(byId[idA]!.quota!.customWindows![0]!.resetAt).toBeDefined();
Expand Down Expand Up @@ -769,7 +769,7 @@ describe("google-antigravity per-account quota (#1082)", () => {
const rows = await fetchProviderAccountQuotas("google-antigravity");
const byId = Object.fromEntries(rows.map(row => [row.accountId, row]));
const [idA, idB] = [idFor("a@example.com"), idFor("b@example.com")];
const windows = (id: string) => byId[id]!.quota!.customWindows!.map(w => `${w.label}=${w.percent}`);
const windows = (id: string) => byId[id]!.quota!.customWindows!.filter(w => w.modelId === undefined).map(w => `${w.label}=${w.percent}`);
expect(windows(idA)).toEqual(["Gem=14", "Cla=62"]);
expect(windows(idB)).toEqual(["Gem=3", "Cla=9"]);
expect(byId[idA]!.quota!.customWindows![0]!.resetAt).toBeDefined();
Expand Down Expand Up @@ -814,8 +814,8 @@ describe("google-antigravity per-account quota (#1082)", () => {
expect(posted.filter(row => row.auth === auth)).toEqual(urls.map(url => ({ url, auth, project, address: "198.18.56.214", tls: true, signal: true })));
}
const byId = Object.fromEntries(rows.map(row => [row.accountId, row]));
expect(byId[idFor("a@example.com")]?.quota?.customWindows?.map(w => w.percent)).toEqual(fallback ? [14, 62] : [14, 14, 62, 62]);
expect(byId[idFor("b@example.com")]?.quota?.customWindows?.map(w => w.percent)).toEqual(fallback ? [3, 9] : [3, 3, 9, 9]);
expect(byId[idFor("a@example.com")]?.quota?.customWindows?.filter(w => w.modelId === undefined).map(w => w.percent)).toEqual(fallback ? [14, 62] : [14, 14, 62, 62]);
expect(byId[idFor("b@example.com")]?.quota?.customWindows?.filter(w => w.modelId === undefined).map(w => w.percent)).toEqual(fallback ? [3, 9] : [3, 3, 9, 9]);
expect(plainFetchCalls).toBe(0);
});
}
Expand Down Expand Up @@ -893,7 +893,31 @@ describe("google-antigravity per-account quota (#1082)", () => {
expect(cached.quotaFailure).toBe("access_denied");
await saveCredential("google-antigravity", { ...credential, access: "agy-third" });
expect(cached.quotaFailureIsCurrent?.()).toBe(false);
expect((await fetchProviderAccountQuotas("google-antigravity"))[0]).not.toHaveProperty("quotaFailure");
expect((await fetchProviderAccountQuotas("google-antigravity"))[0]).toHaveProperty("quotaFailure", "access_denied");
});

test("same-id credential and project replacement invalidates cached model quota measurements", async () => {
const credential = { access: "agy-model-first", refresh: "r1", expires: Date.now() + 3600_000, projectId: "proj-model-first", accountId: "agy-model", email: "model@example.com" };
await saveCredential("google-antigravity", credential);
setAntigravityAccountQuotaTransportForTests({
resolveAddresses: async () => ({ hostname: "daily-cloudcode-pa.googleapis.com", addresses: [{ address: "142.250.0.1", family: 4 }], privateNetwork: false }),
pinnedPost: async url => url.endsWith("retrieveUserQuotaSummary")
? new Response(null, { status: 404 })
: new Response(JSON.stringify({ models: {
"gemini-3.8-flash-high": { quotaInfo: { remainingFraction: 0.76 } },
} }), { status: 200, headers: { "content-type": "application/json" } }),
});

const [row] = await fetchProviderAccountQuotas("google-antigravity", true);
const accountId = idFor("model@example.com");
expect(row?.quota?.customWindows).toContainEqual({
label: "gemini-3.8-flash-high", modelId: "gemini-3.8-flash-high", percent: 24,
});
expect(getCachedProviderAccountQuota("google-antigravity", accountId)?.customWindows)
.toContainEqual({ label: "gemini-3.8-flash-high", modelId: "gemini-3.8-flash-high", percent: 24 });

await saveCredential("google-antigravity", { ...credential, access: "agy-model-second", projectId: "proj-model-second" });
expect(getCachedProviderAccountQuota("google-antigravity", accountId)).toBeNull();
});

test("NO_PROXY denial preserves an unavailable account row without sending its bearer", async () => {
Expand Down Expand Up @@ -1065,7 +1089,7 @@ describe("google-antigravity Fake-IP TUN quota probes without HTTP proxy (#3781)
expect(rows[0]!.accountId).toBe(idFor("a@example.com"));
expect(rows[0]!.unavailable).toBeUndefined();
expect(rows[0]!.quotaFailure).toBeUndefined();
expect(rows[0]!.quota?.customWindows?.map(w => `${w.label}=${w.percent}`)).toEqual(fallback ? ["Gem=14", "Cla=62"] : ["Gem=14", "Gem (Weekly)=14", "Cla=62", "Cla (Weekly)=62"]);
expect(rows[0]!.quota?.customWindows?.filter(w => w.modelId === undefined).map(w => `${w.label}=${w.percent}`)).toEqual(fallback ? ["Gem=14", "Cla=62"] : ["Gem=14", "Gem (Weekly)=14", "Cla=62", "Cla (Weekly)=62"]);
expect(plainFetchCalls).toBe(0);
});
}
Expand Down
Loading
Loading