Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -366,8 +366,9 @@ Providers can expose a built-in shorthand, such as `agy` for `google-antigravity
| `directGeminiWireRenames?` | `boolean` | Google only. Applies in AI Studio mode, including custom Google-compatible URLs. Omitted or `true` keeps the `-tiered` wire rename for Gemini Flash ids (`gemini-3.7-flash` -> `gemini-3.7-flash-tiered`); `false` sends the requested bare ids to the wire unchanged. Vertex and canonical keyed OpenCode Zen routes preserve the requested model ID regardless of this setting; Cloud Code Assist routing is unchanged. Set `false` when the configured upstream still serves the bare ids. |
| `project?` | `string` | Vertex or Antigravity Cloud Code Assist project id. |
| — | — | Antigravity account quota probes (`retrieveUserQuota` and `retrieveUserQuotaSummary`) always go to Google's own Cloud Code host through the pinned outbound transport, regardless of a configured `baseUrl`; the account bearer is never sent to an operator-configured endpoint and a redirect aborts the probe. Only the model-list fallback still honors `baseUrl`. |
| — | — | If Antigravity quota summary returns 403 for a valid OAuth account, OpenCodex retries that endpoint once with the legacy `antigravity/1.0` User-Agent and the same token and project. A 401 is not retried. Inference and model discovery retain the IDE User-Agent. |
| — | — | If Antigravity quota summary returns 403 for a valid OAuth account, OpenCodex retries that endpoint once with the legacy `antigravity/1.0` User-Agent and the same token and project. A 401 is not retried. Usable quota readings also make one optional CLI-User-Agent model-list read when Argon measurements are absent, so returned Argon model quotas appear alongside the Gem/Cla summary. These are display-only measurements; missing or failed Argon reads leave the existing quota intact and do not change account selection or exhaustion. Model discovery uses the CLI User-Agent; inference selects the User-Agent per model. |
| `location?` | `string` | Vertex location; environment fallback is `GOOGLE_CLOUD_LOCATION`. |
| — | — | Argon Low/Medium/High are effort aliases. Matching window metadata, usage and reset are displayed once as `gemini-4-argon`; differing readings stay separate. Values are never added or averaged, and this display grouping does not merge the Gem summary or change routing. |
| `mcpServers?` | `Record<string, CursorMcpServerConfig>` | Cursor only: stdio or Streamable HTTP MCP servers. |
| `desktopExecutor?` | `DesktopExecutorConfig` | Cursor only: external computer-use and record-screen commands. |
| `unsafeAllowNativeLocalExec?` | `boolean` | Cursor legacy boolean, equivalent to `nativeLocalExec: "on"` only when the newer field is unset. |
Expand Down
2 changes: 1 addition & 1 deletion src/oauth/account-quota-rank.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export function accountResetTimestamp(
const quota = getCachedProviderAccountQuota(provider, accountId);
if (!quota) return null;
const family = classifyModelFamilyForQuota(provider, requestedModelId);
const windows = (quota.customWindows ?? []).filter(w => !family || windowMatchesFamily(w.label, family));
const windows = (quota.customWindows ?? []).filter(w => w.modelId === undefined && (!family || windowMatchesFamily(w.label, family)));
const weekly = windows.find(w => /week/i.test(w.label));
let rawReset = weekly?.resetAt ?? quota.weeklyResetAt;
if (rawReset === undefined && provider !== "google-antigravity" && provider !== "anthropic") {
Expand Down
90 changes: 76 additions & 14 deletions src/providers/quota/antigravity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { antigravityUserAgent } from "../../adapters/client-fingerprint";
import { antigravityCliUserAgent, antigravityUserAgent } from "../../adapters/client-fingerprint";
import { DestinationDnsResolutionError } from "../../lib/destination-policy";
import { PinnedHttpError, type PinnedHttpErrorCode } from "../../lib/pinned-http";
import { ProviderOutboundPolicyError, providerOutboundPost, providerRedirectError, type ProviderOutboundDependencies } from "../../lib/provider-outbound";
Expand All @@ -7,6 +7,7 @@ import { getAccountCredential, getCredential } from "../../oauth/store";
import { asRecord, normalizePercent, normalizeResetAt, readQuotaJson, REQUEST_TIMEOUT_MS, toFiniteNumber } from "../quota-wire";
import { report, type ProviderQuotaReport } from "./report-cache";
import { quotaCredentialIdentity } from "./account-cache";
import { canonicalAntigravityUsageModel } from "../antigravity-models";
import type { ProviderQuota, ProviderQuotaWindow, QuotaFailureCode } from "../quota-types";

export function antigravityQuotaDiagnosticIdentity(accountId: string, credential = getAccountCredential("google-antigravity", accountId)): string | undefined {
Expand Down Expand Up @@ -72,7 +73,17 @@ function antigravityWindowsFromModels(body: Record<string, unknown> | null): Pro

const windows = new Map<string, ProviderQuotaWindow>();
const modelWindows: ProviderQuotaWindow[] = [];
const argonReadings = new Map<string, Array<{ window: ProviderQuotaWindow; models: Set<string> }>>();
const labels = new Set(["Gem", "Cla"]);
const uniqueLabel = (baseLabel: string): string => {
let label = baseLabel;
for (let ordinal = 2; labels.has(label); ordinal += 1) {
const suffix = ` #${ordinal}`;
label = `${baseLabel.slice(0, 128 - suffix.length)}${suffix}`;
}
labels.add(label);
return label;
};
for (const [modelId, rawModelInfo] of Object.entries(models)) {
const modelInfo = asRecord(rawModelInfo);
if (!modelInfo) continue;
Expand All @@ -87,23 +98,49 @@ function antigravityWindowsFromModels(body: Record<string, unknown> | null): Pro
// 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 (modelId.trim() && modelId.length <= 128 && modelWindows.length < 256) {
if (modelId.trim() && modelId.length <= 128) {
const tier = typeof quotaInfo.tier === "string" && quotaInfo.tier.length <= 64
? quotaInfo.tier.trim().slice(0, Math.max(0, 125 - modelId.length))
: "";
const baseLabel = `${modelId}${tier ? ` · ${tier}` : ""}`;
let modelLabel = baseLabel;
for (let ordinal = 2; labels.has(modelLabel); ordinal += 1) {
const suffix = ` #${ordinal}`;
modelLabel = `${baseLabel.slice(0, 128 - suffix.length)}${suffix}`;
}
labels.add(modelLabel);
modelWindows.push({
label: modelLabel,
const measurement: ProviderQuotaWindow = {
label: uniqueLabel(`${modelId}${tier ? ` · ${tier}` : ""}`),
modelId,
percent,
...(resetAt !== undefined ? { resetAt } : {}),
});
};
if (canonicalAntigravityUsageModel(modelId) === "gemini-4-argon") {
// Match the full remaining window metadata, not just equal percentages.
const scope = Object.entries(quotaInfo)
.flatMap(([key, value]): Array<[string, unknown]> => {
if (["remainingFraction", "remainingPercentage", "resetTime"].includes(key)) return [];
const remaining = key === "remaining" ? asRecord(value) : null;
if (!remaining) return [[key, value]];
const metadata = Object.fromEntries(Object.entries(remaining).filter(([name]) => !["remainingFraction", "remainingPercentage"].includes(name)));
return Object.keys(metadata).length ? [[key, metadata]] : [];
})
.sort(([a], [b]) => a.localeCompare(b));
const key = JSON.stringify([scope, percent, resetAt ?? quotaInfo.resetTime ?? null], (_name, value: unknown) => {
const record = asRecord(value);
return record ? Object.fromEntries(Object.entries(record).sort(([a], [b]) => a.localeCompare(b))) : value;
});
const readings = argonReadings.get(key) ?? [];
const shared = readings.find(reading => !reading.models.has(modelId));
if (shared) {
labels.delete(measurement.label);
if (shared.models.size === 1) {
labels.delete(shared.window.label);
shared.window.label = uniqueLabel(`gemini-4-argon${tier ? ` · ${tier}` : ""}`);
}
shared.models.add(modelId);
continue;
}
if (modelWindows.length < 256) {
readings.push({ window: measurement, models: new Set([modelId]) });
argonReadings.set(key, readings);
}
}
if (modelWindows.length < 256) modelWindows.push(measurement);
else labels.delete(measurement.label);
}
}
}
Expand Down Expand Up @@ -281,6 +318,31 @@ export async function probeAntigravityUsageQuota(accessToken: string, projectId:
},
body: JSON.stringify({ project: projectId }), signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
}, antigravityOutboundDependencies);
// Summary family bars lack model diagnostics; the IDE list omits CLI-only Argon. Add only
// exact model rows: they remain display evidence, never a new Gem routing counter.
const withArgonMeasurements = async (quota: ProviderQuota): Promise<ProviderQuota> => {
const isArgon = (modelId: string) => canonicalAntigravityUsageModel(modelId) === "gemini-4-argon";
if (quota.customWindows?.some(window => window.modelId !== undefined && isArgon(window.modelId))) return quota;
try {
const response = await fetchQuota(ANTIGRAVITY_QUOTA_MODELS_URL, antigravityCliUserAgent());
if (await providerRedirectError(response, ANTIGRAVITY_QUOTA_MODELS_URL)) return quota;
if (!response.ok) {
try { await response.body?.cancel(); } catch { /* Best-effort release of the optional response. */ }
return quota;
}
const models = asRecord(asRecord(await readQuotaJson(response))?.models);
if (!models) return quota;
const argonModels = Object.fromEntries(Object.entries(models).filter(([modelId]) => isArgon(modelId)));
const measurements = antigravityWindowsFromModels({ models: argonModels }).filter(window => window.modelId !== undefined);
if (!measurements.length) return quota;
let retainedModels = 0;
const previous = (quota.customWindows ?? []).filter(window => window.modelId === undefined || retainedModels++ < 256 - measurements.length);
return { ...quota, customWindows: [...previous, ...measurements] };
} catch {
// An optional display probe cannot invalidate a usable account quota reading.
return quota;
}
};
let summaryFailure: QuotaFailureCode | undefined;
try {
let response = await fetchQuota(ANTIGRAVITY_QUOTA_SUMMARY_URL);
Expand All @@ -293,7 +355,7 @@ export async function probeAntigravityUsageQuota(accessToken: string, projectId:
if (response.status === 401 || response.status === 403) return unavailableAntigravityQuota("access_denied");
if (response.ok) {
const quota = parseAntigravityQuotaSummary(asRecord(await readQuotaJson(response)));
if (quota) return { kind: "available", quota, source: "google-antigravity:retrieveUserQuotaSummary" };
if (quota) return { kind: "available", quota: await withArgonMeasurements(quota), source: "google-antigravity:retrieveUserQuotaSummary" };
}
} catch (error) {
// Existing behavior: summary transport/parse failure may recover through the models probe.
Expand All @@ -311,7 +373,7 @@ export async function probeAntigravityUsageQuota(accessToken: string, projectId:
if (!customWindows.length) {
return unavailableAntigravityQuota(antigravityUnavailableFailure(summaryFailure, "response_unusable"));
}
return { kind: "available", quota: { customWindows, updatedAt: Date.now() }, source: "google-antigravity:fetchAvailableModels" };
return { kind: "available", quota: await withArgonMeasurements({ customWindows, updatedAt: Date.now() }), source: "google-antigravity:fetchAvailableModels" };
} catch (error) {
// The public compatibility wrapper still rejects this exact fallback error; it never enters a DTO.
return {
Expand Down
4 changes: 3 additions & 1 deletion structure/providers-and-adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,9 @@ Antigravity discovery in `src/providers/antigravity-models.ts` collapses complet
Antigravity catalog rows carry the observed complete effort map from `src/providers/antigravity-models.ts` through the in-memory cache. `src/providers/antigravity-effort-families.ts` uses that evidence to group public rows and project suffix allowlists into the base selection, including both final catalog merge callers. Internal retained suffix rows remain available to combo resolution. Authoritative new-model reconciliation normalizes both the prior baseline and policy input before detecting arrivals: an existing suffix family is not a new model. All-disabled tiers transfer a base disable once; a known base keeps the operator's later choice. Genuinely new families still follow new-model policy. Saved defaults, combo targets, suffix disables and provider selections remain unchanged. A discovered ladder advertises only its wire-backed efforts unless explicitly overridden; unknown single-wire models gain no invented ladder.

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. Antigravity model entitlement is per account: one account may list Claude 4.6, another only the Claude 5.5 suffix families, and Gemini 4 Argon only on Ultra/Enterprise. `ANTIGRAVITY_MODELS` is therefore a static superset (Gemini 3.8 Flash, Gemini 4 Argon, Claude Opus 5.5, Claude Sonnet 5.5), and `ANTIGRAVITY_KNOWN_PICKER_MODELS` keeps dropped bases known to routing, discovery collapse and usage identity. The catalog exposes only what the active account's authoritative discovery returns. `src/providers/antigravity-account-roster.ts` records each account's discovered roster (in memory, six-hour freshness, scoped to config home); `src/oauth/generic-account-failover.ts` ranks Antigravity accounts by it the way it ranks Kiro listings: a listing account wins, a known non-listing account is skipped while another remains, and an account without a fresh roster is unknown and stays usable. Gemini 4 Argon requests and Antigravity model discovery send the Antigravity CLI User-Agent; every other model keeps the IDE User-Agent.
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 and distinct labels of at most 128 characters for display and diagnostics; GUI summaries and native compact rows preserve the discriminator, retain details, and exclude model readings from headline and severity selection; account and provider routing ignore these rows and continue to use family summaries, and `src/quota/window-mapping.ts` keeps them out of quota-reset observation so one pool reset notifies once. 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 only current last-good measurements without probing; `src/providers/quota/account-row.ts` preserves private currentness callbacks through account-row projection until management publication.
When the `retrieveUserQuotaSummary` endpoint has no usable result, `src/providers/quota/antigravity.ts` preserves bounded per-model `quotaInfo` measurements from the IDE `fetchAvailableModels` fallback alongside the existing first-seen Gem/Cla family summaries. A usable summary or fallback that has no Argon model measurements makes one optional CLI-User-Agent models read and adds only exact Argon model rows, retaining the 256-model-row ceiling and all family counters; an absent or failed supplement leaves the usable reading intact. Model windows carry the exact CCA wire ID and distinct labels of at most 128 characters for display and diagnostics; GUI summaries and native compact rows preserve the discriminator, retain details, and exclude model readings from headline and severity selection; account and provider routing ignore these rows and continue to use family summaries, and `src/quota/window-mapping.ts` keeps them out of quota-reset observation so one pool reset notifies once. 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 only current last-good measurements without probing; `src/providers/quota/account-row.ts` preserves private currentness callbacks through account-row projection until management publication.

Identical Argon effort-alias readings collapse to one `gemini-4-argon` display row only when their window metadata, percentage and reset agree; one exact wire `modelId` remains as the diagnostic discriminator. Different counters, reset times or window metadata stay separate, as do repeated windows from the same wire ID. This display deduplication does not combine the Gem family summaries or add metadata requests.

Account-row projection is loaded with `import()` after each account probe in `src/providers/quota.ts`; its cache dependencies do not create a static return edge into the quota facade.

Expand Down
2 changes: 1 addition & 1 deletion structure/transports/inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ Pool quota producers and account commands follow the [bounded raw-observation co

## Account quota failure diagnostics

`src/providers/quota/antigravity.ts` retries a quota-summary 403 once with `User-Agent: antigravity/1.0`, releasing the first response body and preserving the bearer, project and pinned accounting endpoint. A 401 is not retried; redirects remain blocked, and a repeated 403 remains unavailable with `access_denied`. Other retry failures retain the models fallback, whose User-Agent remains the IDE fingerprint, as do discovery and inference.
`src/providers/quota/antigravity.ts` retries a quota-summary 403 once with `User-Agent: antigravity/1.0`, releasing the first response body and preserving the bearer, project and pinned accounting endpoint. A 401 is not retried; redirects remain blocked, and a repeated 403 remains unavailable with `access_denied`. Other retry failures retain the IDE models fallback. A usable reading may additionally collect CLI-only Argon model diagnostics with the CLI fingerprint through the same pinned accounting endpoint; supplement failure cannot invalidate that reading. General model discovery uses the CLI fingerprint; inference selects the fingerprint per wire model.

Antigravity account quota probes expose only a closed `quotaFailure` category when the read is unavailable. Typed transport failures, rejected destinations, redirects, denied access, rate limits and unusable bodies are distinguished; successful fallback clears the earlier failure. The last attempted endpoint determines the diagnosis. A 401/403 category does not change account health, entitlement or routing eligibility.

Expand Down
Loading
Loading