Skip to content
Merged
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 @@ -177,6 +177,7 @@ predictions. Explicit provider/model price overrides still take precedence.
| `googleMode?` | `"ai-studio" \| "vertex" \| "cloud-code-assist"` | Google transport/auth mode. Default `ai-studio`. |
| `directGeminiWireRenames?` | `boolean` | Google only. Applies only to direct AI Studio requests. 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 preserves the requested model ID, and 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`. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Correct the claim that the bearer never reaches baseUrl

When retrieveUserQuotaSummary returns a non-success response other than 401/403, throws, or contains no recognized windows, fetchAntigravityQuota falls through to the model-list request at src/providers/quota.ts:2577-2584; that request uses the operator-configured baseUrl, includes the Google bearer, and follows redirects by default. The new statement that the bearer is never sent to such an endpoint is therefore false precisely during summary fallback and may encourage an unsafe custom configuration; either pin the fallback as well or explicitly document that credential exposure exception.

AGENTS.md reference: docs-site/AGENTS.md:L7-L10

Useful? React with 👍 / 👎.

| `location?` | `string` | Vertex location; environment fallback is `GOOGLE_CLOUD_LOCATION`. |
| `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
217 changes: 213 additions & 4 deletions src/providers/quota.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { resolveProviderApiKey } from "./key-store";
import { getValidAccessToken, getValidAccessTokenForAccount } from "../oauth";
import { getAccountCredential, getAccountSet, getCredential } from "../oauth/store";
import { antigravityUserAgent } from "../adapters/client-fingerprint";
import { isCanonicalOllamaCloudUrl } from "../adapters/ollama-native-url";
import { providerOutboundPost, providerRedirectError, type ProviderOutboundDependencies } from "../lib/provider-outbound";
import { apiKeyPoolEntryId } from "./api-keys";
import { XAI_GROK_CLIENT_VERSION, XAI_GROK_COMPATIBILITY } from "./xai-transport";
Expand Down Expand Up @@ -83,6 +84,8 @@ const OPENCODE_GO_USAGE_URL = `${OPENCODE_GO_BASE_URL}/usage`;
const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
const DEEPSEEK_BASE_URL = "https://api.deepseek.com";
const CLINE_BASE_URL = "https://api.cline.bot";
const OLLAMA_CLOUD_BASE_URL = "https://ollama.com";
const OLLAMA_CLOUD_USAGE_URL = `${OLLAMA_CLOUD_BASE_URL}/api/usage`;
const ZAI_BASE_URL = "https://api.z.ai";
const ZAI_CN_BASE_URL = "https://open.bigmodel.cn";
const MINIMAX_REMAINS_URL = "https://www.minimax.io/v1/token_plan/remains";
Expand Down Expand Up @@ -328,6 +331,15 @@ function isCanonicalClineBaseUrl(baseUrl: string): boolean {
return normalized === CLINE_BASE_URL || normalized === `${CLINE_BASE_URL}/api/v1`;
}

function isCanonicalOllamaCloudBaseUrl(baseUrl?: string): boolean {
if (!baseUrl) return false;
try {
return isCanonicalOllamaCloudUrl(baseUrl);
} catch {
return false;
}
}

function isCanonicalZaiBaseUrl(baseUrl: string): boolean {
const normalized = normalizedBaseUrl(baseUrl);
return normalized === ZAI_BASE_URL
Expand Down Expand Up @@ -659,6 +671,78 @@ async function fetchClineQuota(provider: string, config: OcxProviderConfig): Pro
return windows > 0 ? report(provider, "cline:plan-usage-limits", quota) : null;
}

/**
* Ollama Cloud `GET https://ollama.com/api/usage` — returns account usage.
* Legacy plans report rolling 5-hour `limits.session.usage` and 7-day
* `limits.weekly.usage`. Migrated monthly-credit plans report
* `limits.monthly.usage`. `usage` values are normalized fractions (0..1).
*/
function parseOllamaPercent(usageValue: unknown): number | undefined {
const usage = toFiniteNumber(usageValue);
if (usage === undefined || usage < 0) return undefined;
const percent = Math.round(usage * 10000) / 100;
return normalizePercent(percent);
}

export function parseOllamaCloudQuota(body: Record<string, unknown> | null): ProviderQuota | null {
if (!body) return null;
const limits = asRecord(body.limits);
if (!limits) return null;

const quota: ProviderQuota = { updatedAt: Date.now() };
let windows = 0;

const session = asRecord(limits.session);
if (session) {
const percent = parseOllamaPercent(session.usage);
if (percent !== undefined) {
quota.fiveHourPercent = percent;
windows += 1;
}
}

const weekly = asRecord(limits.weekly);
if (weekly) {
const percent = parseOllamaPercent(weekly.usage);
if (percent !== undefined) {
quota.weeklyPercent = percent;
windows += 1;
}
}

const monthly = asRecord(limits.monthly);
if (monthly) {
const percent = parseOllamaPercent(monthly.usage);
if (percent !== undefined) {
quota.monthlyPercent = percent;
windows += 1;
}
}

return windows > 0 ? quota : null;
}

async function fetchOllamaCloudQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
const effectiveBaseUrl = config.baseUrl ?? getProviderRegistryEntry(provider)?.baseUrl ?? "";
if (!isCanonicalOllamaCloudBaseUrl(effectiveBaseUrl)) return null;
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
if (!apiKey) return null;
const response = await fetch(OLLAMA_CLOUD_USAGE_URL, {
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
redirect: "error",
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
});
if (!response.ok) {
if (response.status === 404) return null;
return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
? TERMINAL_QUOTA_FAILURE
: null;
}
const body = asRecord(await readQuotaJson(response));
const quota = parseOllamaCloudQuota(body);
return quota ? report(provider, "ollama-cloud:usage", quota) : null;
}

/**
* Z.AI GLM Coding Plan `GET /api/monitor/usage/quota/limit` — the coding-plan
* limits arrive as a `limits` array of `TOKENS_LIMIT` (newer plans call the
Expand Down Expand Up @@ -2287,10 +2371,11 @@ function classifyAntigravityFamily(modelId: string, modelInfo: Record<string, un
}

function antigravityUsedPercent(quotaInfo: Record<string, unknown>): number | undefined {
const remaining = normalizePercent(toFiniteNumber(quotaInfo.remainingFraction) !== undefined
? toFiniteNumber(quotaInfo.remainingFraction)! * 100
: toFiniteNumber(quotaInfo.remainingPercentage) !== undefined
? toFiniteNumber(quotaInfo.remainingPercentage)! * 100
const target = asRecord(quotaInfo.remaining) ?? quotaInfo;
const remaining = normalizePercent(toFiniteNumber(target.remainingFraction) !== undefined
? toFiniteNumber(target.remainingFraction)! * 100
: toFiniteNumber(target.remainingPercentage) !== undefined
? toFiniteNumber(target.remainingPercentage)! * 100
: undefined);
if (remaining === undefined) return undefined;
return normalizePercent(100 - remaining);
Expand Down Expand Up @@ -2325,6 +2410,75 @@ function antigravityWindowsFromModels(body: Record<string, unknown> | null): Pro
return customWindows;
}

/**
* Parse Google Antigravity quota from `v1internal:retrieveUserQuotaSummary`.
* Groups contain Gemini models and Claude/3P models, each with 5h and weekly limit buckets.
*/
function parseAntigravityQuotaSummary(body: Record<string, unknown> | null): ProviderQuota | null {
const groups = Array.isArray(body?.groups) ? (body.groups as unknown[]) : [];
if (groups.length === 0) return null;

const customWindowsMap = new Map<string, ProviderQuotaWindow>();

for (const rawGroup of groups) {
const group = asRecord(rawGroup);
if (!group) continue;
const groupName = `${typeof group.displayName === "string" ? group.displayName : ""} ${typeof group.description === "string" ? group.description : ""}`.toLowerCase();
const isGemini = groupName.includes("gemini");
const isClaude = groupName.includes("claude") || groupName.includes("3p") || groupName.includes("gpt");

const buckets = Array.isArray(group.buckets) ? (group.buckets as unknown[]) : [];
for (const rawBucket of buckets) {
const bucket = asRecord(rawBucket);
if (!bucket) continue;
const windowStr = `${typeof bucket.window === "string" ? bucket.window : ""} ${typeof bucket.bucketId === "string" ? bucket.bucketId : ""} ${typeof bucket.displayName === "string" ? bucket.displayName : ""}`.toLowerCase();
const percent = antigravityUsedPercent(bucket);
if (percent === undefined) continue;
const resetAt = normalizeResetAt(bucket.resetTime);

const isWeekly = windowStr.includes("week");
const is5h = windowStr.includes("5h") || windowStr.includes("five");

if (isGemini) {
const label = is5h ? "Gem" : isWeekly ? "Gem (Weekly)" : "";
if (label && !customWindowsMap.has(label)) {
customWindowsMap.set(label, { label, percent, ...(resetAt !== undefined ? { resetAt } : {}) });
}
} else if (isClaude) {
const label = is5h ? "Cla" : isWeekly ? "Cla (Weekly)" : "";
if (label && !customWindowsMap.has(label)) {
customWindowsMap.set(label, { label, percent, ...(resetAt !== undefined ? { resetAt } : {}) });
}
} else {
const baseLabel = typeof group.displayName === "string" ? group.displayName : "Other";
const label = isWeekly ? `${baseLabel} (Weekly)` : baseLabel;
if (!customWindowsMap.has(label)) {
customWindowsMap.set(label, { label, percent, ...(resetAt !== undefined ? { resetAt } : {}) });
}
}
}
}

const PREFERRED_ORDER = ["Gem", "Gem (Weekly)", "Cla", "Cla (Weekly)"];
const customWindows = Array.from(customWindowsMap.values()).sort((a, b) => {
const ia = PREFERRED_ORDER.indexOf(a.label);
const ib = PREFERRED_ORDER.indexOf(b.label);
if (ia !== -1 && ib !== -1) return ia - ib;
if (ia !== -1) return -1;
if (ib !== -1) return 1;
return a.label.localeCompare(b.label);
});

if (customWindows.length === 0) {
return null;
}

return {
customWindows,
updatedAt: Date.now(),
};
}

const ANTIGRAVITY_ACCOUNT_QUOTA_BASE = "https://daily-cloudcode-pa.googleapis.com";
let antigravityOutboundDependencies: ProviderOutboundDependencies = {};

Expand All @@ -2341,6 +2495,28 @@ export function setAntigravityAccountQuotaTransportForTests(dependencies: Provid
* A redirect or non-2xx yields null (unavailable), never a partial row.
*/
export async function fetchAntigravityUsageQuota(accessToken: string, projectId: string): Promise<ProviderQuota | null> {
const summaryUrl = `${ANTIGRAVITY_ACCOUNT_QUOTA_BASE}/v1internal:retrieveUserQuotaSummary`;
try {
const summaryResponse = await providerOutboundPost("google-antigravity", { baseUrl: ANTIGRAVITY_ACCOUNT_QUOTA_BASE }, summaryUrl, {
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent": antigravityUserAgent(),
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({ project: projectId }),
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
}, antigravityOutboundDependencies);
if (await providerRedirectError(summaryResponse, summaryUrl)) return null;
if (summaryResponse.status === 401 || summaryResponse.status === 403) return null;
if (summaryResponse.ok) {
const quota = parseAntigravityQuotaSummary(asRecord(await readQuotaJson(summaryResponse)));
if (quota) return quota;
}
} catch {
// Fallback to fetchAvailableModels on error
}

const url = `${ANTIGRAVITY_ACCOUNT_QUOTA_BASE}/v1internal:fetchAvailableModels`;
const response = await providerOutboundPost("google-antigravity", { baseUrl: ANTIGRAVITY_ACCOUNT_QUOTA_BASE }, url, {
headers: {
Expand Down Expand Up @@ -2369,6 +2545,35 @@ async function fetchAntigravityQuota(provider: string, config: OcxProviderConfig
return null;
}
const baseUrl = (config.baseUrl || ANTIGRAVITY_ACCOUNT_QUOTA_BASE).replace(/\/+$/, "");

// The summary probe is pinned to Google's own host through the provider-outbound
// transport, mirroring `fetchAntigravityUsageQuota` above: a configured `baseUrl` is a
// routing choice for requests, not a second source of Google's accounting, and this
// request carries the account bearer.
const summaryUrl = `${ANTIGRAVITY_ACCOUNT_QUOTA_BASE}/v1internal:retrieveUserQuotaSummary`;
try {
const summaryResponse = await providerOutboundPost("google-antigravity", { baseUrl: ANTIGRAVITY_ACCOUNT_QUOTA_BASE }, summaryUrl, {
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent": antigravityUserAgent(),
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({ project: credential.projectId }),
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
}, antigravityOutboundDependencies);
if (await providerRedirectError(summaryResponse, summaryUrl)) return null;
if (summaryResponse.status === 401 || summaryResponse.status === 403) return null;
if (summaryResponse.ok) {
const quota = parseAntigravityQuotaSummary(asRecord(await readQuotaJson(summaryResponse)));
if (quota) {
return report(provider, "google-antigravity:retrieveUserQuotaSummary", quota);
}
}
} catch {
// Fallback on network/fetch error
}

const response = await fetch(`${baseUrl}/v1internal:fetchAvailableModels`, {
method: "POST",
headers: {
Expand Down Expand Up @@ -2446,6 +2651,10 @@ async function maybeFetchProviderQuota(
if ((provider.authMode ?? "key") === "key" && name === "cline-pass") {
return fetchClineQuota(name, provider);
}
if ((provider.authMode ?? "key") === "key"
&& (name === "ollama-cloud" || isCanonicalOllamaCloudBaseUrl(provider.baseUrl))) {
return fetchOllamaCloudQuota(name, provider);
}
if ((provider.authMode ?? "key") === "key"
&& (name === "zai" || name === "glm" || name === "glm-cn" || name === "zhipu-bigmodel-coding")) {
return fetchZaiQuota(name, provider);
Expand Down
62 changes: 57 additions & 5 deletions tests/providers/provider-account-quota.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,9 +438,30 @@ describe("google-antigravity per-account quota (#1082)", () => {
});
}

function antigravitySummaryBody(gemRemaining: number, claRemaining: number): string {
return JSON.stringify({
groups: [
{
displayName: "Gemini Models",
buckets: [
{ bucketId: "gemini-weekly", window: "weekly", remainingFraction: gemRemaining, resetTime: "2026-09-09T12:00:00Z" },
{ bucketId: "gemini-5h", window: "5h", remainingFraction: gemRemaining, resetTime: "2026-09-02T12:00:00Z" },
],
},
{
displayName: "Claude and GPT models",
buckets: [
{ bucketId: "3p-weekly", window: "weekly", remainingFraction: claRemaining, resetTime: "2026-09-09T18:00:00Z" },
{ bucketId: "3p-5h", window: "5h", remainingFraction: claRemaining, resetTime: "2026-09-02T18:00:00Z" },
],
},
],
});
}

afterEach(() => setAntigravityAccountQuotaTransportForTests(null));

test("probes each account with its own bearer and project id on the fixed Google host over the pinned transport", async () => {
test("probes each account with its own bearer and project id on the fixed Google host using retrieveUserQuotaSummary", async () => {
const expires = Date.now() + 60 * 60_000;
await saveCredential("google-antigravity", { access: "agy-first", refresh: "r1", expires, projectId: "proj-first", accountId: "agy-a", email: "a@example.com" });
await saveCredential("google-antigravity", { access: "agy-second", refresh: "r2", expires, projectId: "proj-second", accountId: "agy-b", email: "b@example.com" });
Expand All @@ -453,6 +474,9 @@ describe("google-antigravity per-account quota (#1082)", () => {
const auth = new Headers(requestOptions?.headers).get("authorization") ?? "";
const project = String(JSON.parse(String(body)).project);
seen.push({ url, auth, project, address: pinned.address });
if (url.endsWith("retrieveUserQuotaSummary")) {
return new Response(auth.endsWith("agy-first") ? antigravitySummaryBody(0.86, 0.38) : antigravitySummaryBody(0.97, 0.91), { status: 200, headers: { "content-type": "application/json" } });
}
return new Response(auth.endsWith("agy-first") ? antigravityBody(0.86, 0.38) : antigravityBody(0.97, 0.91), { status: 200, headers: { "content-type": "application/json" } });
},
});
Expand All @@ -463,16 +487,45 @@ describe("google-antigravity per-account quota (#1082)", () => {
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}`);
expect(windows(idA)).toEqual(["Gem=14", "Cla=62"]);
expect(windows(idB)).toEqual(["Gem=3", "Cla=9"]);
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();
expect(seen.map(s => `${s.auth}|${s.project}`).sort()).toEqual(["Bearer agy-first|proj-first", "Bearer agy-second|proj-second"]);
for (const s of seen) {
expect(s.url).toBe("https://daily-cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels");
expect(s.url).toBe("https://daily-cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary");
expect(s.address).toBe("142.250.0.1");
}
});

test("falls back to fetchAvailableModels when retrieveUserQuotaSummary returns 404", async () => {
const expires = Date.now() + 60 * 60_000;
await saveCredential("google-antigravity", { access: "agy-first", refresh: "r1", expires, projectId: "proj-first", accountId: "agy-a", email: "a@example.com" });
await saveCredential("google-antigravity", { access: "agy-second", refresh: "r2", expires, projectId: "proj-second", accountId: "agy-b", email: "b@example.com" });
globalThis.fetch = (async () => { throw new Error("plain fetch must not be used for account bearers"); }) as typeof fetch;

const seen: Array<{ url: string; auth: string; project: string; address: string }> = [];
setAntigravityAccountQuotaTransportForTests({
resolveAddresses: async () => ({ hostname: "daily-cloudcode-pa.googleapis.com", addresses: [{ address: "142.250.0.1", family: 4 }], privateNetwork: false }),
pinnedPost: async (url, pinned, body, _signal, requestOptions) => {
const auth = new Headers(requestOptions?.headers).get("authorization") ?? "";
const project = String(JSON.parse(String(body)).project);
seen.push({ url, auth, project, address: pinned.address });
if (url.endsWith("retrieveUserQuotaSummary")) {
return new Response(null, { status: 404 });
}
return new Response(auth.endsWith("agy-first") ? antigravityBody(0.86, 0.38) : antigravityBody(0.97, 0.91), { status: 200, headers: { "content-type": "application/json" } });
},
});

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}`);
expect(windows(idA)).toEqual(["Gem=14", "Cla=62"]);
expect(windows(idB)).toEqual(["Gem=3", "Cla=9"]);
expect(byId[idA]!.quota!.customWindows![0]!.resetAt).toBeDefined();
});

test("a rejected destination never receives a bearer; the row is unavailable, not 0%", async () => {
const expires = Date.now() + 60 * 60_000;
await saveCredential("google-antigravity", { access: "agy-first", refresh: "r1", expires, projectId: "proj-first", accountId: "agy-a", email: "a@example.com" });
Expand Down Expand Up @@ -506,4 +559,3 @@ describe("google-antigravity per-account quota (#1082)", () => {
}
});
});

Loading
Loading