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 @@ -369,8 +369,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. Vertex API keys use this project endpoint when set; without it they use express mode, even if `GOOGLE_CLOUD_PROJECT` is set. ADC falls back to `GOOGLE_CLOUD_PROJECT` / `GCLOUD_PROJECT`. |
| — | — | 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. Model discovery uses the CLI User-Agent; inference selects the User-Agent per model. If a usable quota summary or IDE models fallback has no model-tagged Argon measurement, one optional CLI-User-Agent models read adds exact Argon diagnostics. A tagged `Argon` / `Argon (Weekly)` summary window skips that read; a label alone does not. Missing or failed supplements keep the usable quota intact. |
| — | — | Antigravity quota shows Gemini 4 Argon as separate `Argon` / `Argon (Weekly)` windows next to Gem and Cla. They are display-only: account ranking, quota exhaustion, reset ordering and reset notifications keep using the Gemini and Claude windows, and Argon requests are still judged by the Gemini window. |
| — | — | 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. |
| `location?` | `string` | Vertex location. API-key project endpoints default to `global` and ignore `GOOGLE_CLOUD_LOCATION`; ADC requires this field or the `GOOGLE_CLOUD_LOCATION` fallback. Gemini 4 Argon supports `global`, `us`, and `eu`. |
| `mcpServers?` | `Record<string, CursorMcpServerConfig>` | Cursor only: stdio or Streamable HTTP MCP servers. |
| `desktopExecutor?` | `DesktopExecutorConfig` | Cursor only: external computer-use and record-screen commands. |
Expand Down
96 changes: 82 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 @@ -95,7 +96,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 @@ -110,23 +121,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 @@ -306,6 +343,12 @@ function antigravityUnavailableFailure(
return fallbackFailure;
}

/** Only model-attributed evidence can suppress an optional Argon diagnostics read. */
export function hasAntigravityArgonMeasurements(quota: ProviderQuota): boolean {
return quota.customWindows?.some(window => window.modelId !== undefined
&& canonicalAntigravityUsageModel(window.modelId) === ARGON_POOL_MODEL_ID) ?? false;
}

export async function probeAntigravityUsageQuota(accessToken: string, projectId: string): Promise<AntigravityQuotaProbeResult> {
const fetchQuota = (url: string, userAgent = antigravityUserAgent()) => providerOutboundPost("google-antigravity", { baseUrl: ANTIGRAVITY_ACCOUNT_QUOTA_BASE }, url, {
headers: {
Expand All @@ -314,6 +357,31 @@ export async function probeAntigravityUsageQuota(accessToken: string, projectId:
},
body: JSON.stringify({ project: projectId }), signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
}, antigravityOutboundDependencies);
// A tagged summary pool already supplies Argon; older summaries and the IDE list may not.
// Add only exact model rows, never a new Gem routing counter or evidence from labels alone.
const withArgonMeasurements = async (quota: ProviderQuota): Promise<ProviderQuota> => {
const isArgon = (modelId: string) => canonicalAntigravityUsageModel(modelId) === "gemini-4-argon";
if (hasAntigravityArgonMeasurements(quota)) 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 @@ -326,7 +394,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 @@ -344,7 +412,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.
`retrieveUserQuotaSummary` reports Gemini 4 Argon as its own group; `src/providers/quota/antigravity.ts` decides each bucket's family from its own id/name (Argon before Gemini, then Claude/3P/GPT), and only for an inconclusive bucket falls back to the group: Argon from the group display name alone (never its description), Gemini and Claude/3P/GPT from the existing display-name-plus-description rules. Argon buckets become `Argon`/`Argon (Weekly)` windows tagged with the canonical pool model `gemini-4-argon`, so they never fill or replace Gem/Cla in either group order and stay diagnostic like the model windows below; an Argon-only summary is a display report with no routing evidence, and the compact GUI summary and tray aggregation treat it like other diagnostic-only data. When the summary endpoint has no usable result, the same file preserves bounded per-model `quotaInfo` measurements from `fetchAvailableModels` alongside the existing first-seen Gem/Cla family summaries, where an Argon model never seeds Gem. 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.
`retrieveUserQuotaSummary` reports Gemini 4 Argon as its own group; `src/providers/quota/antigravity.ts` decides each bucket's family from its own id/name (Argon before Gemini, then Claude/3P/GPT), and only for an inconclusive bucket falls back to the group: Argon from the group display name alone (never its description), Gemini and Claude/3P/GPT from the existing display-name-plus-description rules. Argon buckets become `Argon`/`Argon (Weekly)` windows tagged with the canonical pool model `gemini-4-argon`, so they never fill or replace Gem/Cla in either group order and stay diagnostic like the model windows below; an Argon-only summary is a display report with no routing evidence, and the compact GUI summary and tray aggregation treat it like other diagnostic-only data. When the summary endpoint has no usable result, the same file preserves bounded per-model `quotaInfo` measurements from `fetchAvailableModels` alongside the existing first-seen Gem/Cla family summaries, where an Argon model never seeds Gem. A usable summary or fallback without model-tagged Argon 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. The canonical tagged Argon summary pool skips that read; a label alone does not. 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
Loading
Loading