From 1257b8b21b1dad310a367931082efc12af25bfbb Mon Sep 17 00:00:00 2001 From: yhualin Date: Fri, 4 Sep 2026 16:43:05 +0800 Subject: [PATCH 1/4] feat(quota): support Google Antigravity weekly quota and Ollama Cloud quota --- src/providers/quota.ts | 212 +++++++++++- .../providers/provider-account-quota.test.ts | 62 +++- tests/providers/provider-quota.test.ts | 302 ++++++++++++++++++ 3 files changed, 567 insertions(+), 9 deletions(-) diff --git a/src/providers/quota.ts b/src/providers/quota.ts index 9cdaafe6b6..c2911a2129 100644 --- a/src/providers/quota.ts +++ b/src/providers/quota.ts @@ -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"; @@ -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"; @@ -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 @@ -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 | 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 { + 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 @@ -2287,10 +2371,11 @@ function classifyAntigravityFamily(modelId: string, modelInfo: Record): 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); @@ -2325,6 +2410,75 @@ function antigravityWindowsFromModels(body: Record | 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 | null): ProviderQuota | null { + const groups = Array.isArray(body?.groups) ? (body.groups as unknown[]) : []; + if (groups.length === 0) return null; + + const customWindowsMap = new Map(); + + 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 = {}; @@ -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 { + 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: { @@ -2369,6 +2545,30 @@ async function fetchAntigravityQuota(provider: string, config: OcxProviderConfig return null; } const baseUrl = (config.baseUrl || ANTIGRAVITY_ACCOUNT_QUOTA_BASE).replace(/\/+$/, ""); + + try { + const summaryResponse = await fetch(`${baseUrl}/v1internal:retrieveUserQuotaSummary`, { + method: "POST", + 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), + }); + 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: { @@ -2446,6 +2646,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); diff --git a/tests/providers/provider-account-quota.test.ts b/tests/providers/provider-account-quota.test.ts index ccebd5c032..0f0b242df6 100644 --- a/tests/providers/provider-account-quota.test.ts +++ b/tests/providers/provider-account-quota.test.ts @@ -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" }); @@ -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" } }); }, }); @@ -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" }); @@ -506,4 +559,3 @@ describe("google-antigravity per-account quota (#1082)", () => { } }); }); - diff --git a/tests/providers/provider-quota.test.ts b/tests/providers/provider-quota.test.ts index 582b03b457..59eefeb265 100644 --- a/tests/providers/provider-quota.test.ts +++ b/tests/providers/provider-quota.test.ts @@ -13,6 +13,7 @@ import { saveCredential } from "../../src/oauth/store"; import { clearProviderQuotaCache, fetchProviderQuotaReports, + parseOllamaCloudQuota, parseXaiCreditsResponse, QUOTA_RESPONSE_MAX_BYTES, readProviderQuotaJsonForTests, @@ -2801,4 +2802,305 @@ describe("fetchProviderQuotaReports", () => { const pruned = await fetchProviderQuotaReports(disabledConfig, true); expect(pruned.reports).toEqual([]); }); + + test("Google Antigravity maps 5-hour and weekly quota from retrieveUserQuotaSummary", async () => { + await saveCredential("google-antigravity", { + access: "agy-summary-access", + refresh: "agy-summary-refresh", + expires: Date.now() + 3600_000, + projectId: "agy-summary-project", + }); + + const seen: Array<{ url: string; auth?: string; body?: string }> = []; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const url = String(input); + const headers = init?.headers as Record | undefined; + seen.push({ url, auth: headers?.Authorization, body: typeof init?.body === "string" ? init.body : undefined }); + + if (url.endsWith("retrieveUserQuotaSummary")) { + return new Response(JSON.stringify({ + groups: [ + { + displayName: "Gemini Models", + description: "Models within this group: Gemini Flash, Gemini Pro", + buckets: [ + { + bucketId: "gemini-weekly", + displayName: "Weekly Limit Remaining", + window: "weekly", + resetTime: "2026-09-11T02:25:41Z", + remainingFraction: 0.88, + }, + { + bucketId: "gemini-5h", + displayName: "Five Hour Limit Remaining", + window: "5h", + resetTime: "2026-09-04T12:25:41Z", + remainingFraction: 0.75, + }, + ], + }, + { + displayName: "Claude and GPT models", + description: "Models within this group: Claude Opus, Claude Sonnet, GPT-OSS", + buckets: [ + { + bucketId: "3p-weekly", + displayName: "Weekly Limit Remaining", + window: "weekly", + resetTime: "2026-09-04T10:57:22Z", + remainingFraction: 0.95, + }, + { + bucketId: "3p-5h", + displayName: "Five Hour Limit Remaining", + window: "5h", + resetTime: "2026-09-04T12:54:45Z", + remainingFraction: 1.0, + }, + ], + }, + ], + }), { status: 200, headers: { "content-type": "application/json" } }); + } + return new Response("not found", { status: 404 }); + }) as typeof fetch; + + const config = { + defaultProvider: "google-antigravity", + providers: { + "google-antigravity": { + adapter: "google", + authMode: "oauth", + baseUrl: "https://daily-cloudcode-pa.googleapis.com", + }, + }, + } as unknown as OcxConfig; + + const result = await fetchProviderQuotaReports(config, true); + expect(result.reports).toHaveLength(1); + const report = result.reports[0]!; + expect(report.provider).toBe("google-antigravity"); + expect(report.source).toBe("google-antigravity:retrieveUserQuotaSummary"); + expect(report.quota.customWindows).toEqual([ + { label: "Gem", percent: 25, resetAt: Date.parse("2026-09-04T12:25:41Z") }, + { label: "Gem (Weekly)", percent: 12, resetAt: Date.parse("2026-09-11T02:25:41Z") }, + { label: "Cla", percent: 0, resetAt: Date.parse("2026-09-04T12:54:45Z") }, + { label: "Cla (Weekly)", percent: 5, resetAt: Date.parse("2026-09-04T10:57:22Z") }, + ]); + expect(seen[0]?.url).toBe("https://daily-cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary"); + expect(seen[0]?.auth).toBe("Bearer agy-summary-access"); + expect(seen[0]?.body).toBe(JSON.stringify({ project: "agy-summary-project" })); + }); + + test("Google Antigravity supports nested remaining object and deduplicates custom windows", async () => { + await saveCredential("google-antigravity", { + access: "agy-nested-access", + refresh: "agy-nested-refresh", + expires: Date.now() + 3600_000, + projectId: "agy-nested-project", + }); + + globalThis.fetch = (async (input: RequestInfo | URL) => { + const url = String(input); + if (url.endsWith("retrieveUserQuotaSummary")) { + return new Response(JSON.stringify({ + groups: [ + { + displayName: "Gemini Models", + buckets: [ + { + bucketId: "gemini-weekly", + window: "weekly", + resetTime: "2026-09-11T00:00:00Z", + remaining: { remainingFraction: 0.70 }, + }, + { + bucketId: "gemini-5h", + window: "5h", + resetTime: "2026-09-04T08:00:00Z", + remaining: { remainingFraction: 0.60 }, + }, + ], + }, + { + displayName: "Claude and GPT models", + buckets: [ + { + bucketId: "claude-3p-5h-1", + displayName: "Claude 5 Hour Limit", + window: "5h", + resetTime: "2026-09-04T09:00:00Z", + remaining: { remainingFraction: 0.90 }, + }, + { + bucketId: "claude-3p-5h-duplicate", + displayName: "GPT 5 Hour Limit", + window: "5h", + resetTime: "2026-09-04T09:30:00Z", + remaining: { remainingFraction: 0.50 }, + }, + { + bucketId: "claude-3p-weekly", + displayName: "Claude Weekly Limit", + window: "weekly", + resetTime: "2026-09-11T09:00:00Z", + remaining: { remainingFraction: 0.85 }, + }, + ], + }, + ], + }), { status: 200, headers: { "content-type": "application/json" } }); + } + return new Response("not found", { status: 404 }); + }) as typeof fetch; + + const config = { + defaultProvider: "google-antigravity", + providers: { + "google-antigravity": { + adapter: "google", + authMode: "oauth", + baseUrl: "https://daily-cloudcode-pa.googleapis.com", + }, + }, + } as unknown as OcxConfig; + + const result = await fetchProviderQuotaReports(config, true); + expect(result.reports).toHaveLength(1); + const report = result.reports[0]!; + expect(report.quota.customWindows).toEqual([ + { label: "Gem", percent: 40, resetAt: Date.parse("2026-09-04T08:00:00Z") }, + { label: "Gem (Weekly)", percent: 30, resetAt: Date.parse("2026-09-11T00:00:00Z") }, + { label: "Cla", percent: 10, resetAt: Date.parse("2026-09-04T09:00:00Z") }, + { label: "Cla (Weekly)", percent: 15, resetAt: Date.parse("2026-09-11T09:00:00Z") }, + ]); + }); + + test("Ollama Cloud maps 5-hour session and weekly windows from /api/usage (legacy plan)", async () => { + const seen: Array<{ url: string; authorization?: string; redirect?: RequestRedirect }> = []; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const url = String(input); + const headers = init?.headers as Record | undefined; + seen.push({ url, authorization: headers?.Authorization, redirect: init?.redirect }); + return new Response(JSON.stringify({ + activity: { cost: "0.00000", period: { type: "last_4_weeks" }, models: [] }, + limits: { + session: { usage: 0.091, models: [{ name: "glm-5.3-flash", request_count: 228 }] }, + weekly: { usage: 0.592, models: [{ name: "glm-5.3", request_count: 2572 }] }, + }, + }), { status: 200 }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports( + keyQuotaConfig("ollama-cloud", "https://ollama.com/v1"), + true, + ); + + expect(result.reports).toHaveLength(1); + expect(result.reports[0]?.source).toBe("ollama-cloud:usage"); + expect(result.reports[0]?.quota).toMatchObject({ + fiveHourPercent: 9.1, + weeklyPercent: 59.2, + }); + expect(seen).toHaveLength(1); + expect(seen[0]?.url).toBe("https://ollama.com/api/usage"); + expect(seen[0]?.authorization).toBe("Bearer ollama-cloud-secret"); + expect(seen[0]?.redirect).toBe("error"); + }); + + test("Ollama Cloud maps monthly window from /api/usage (migrated plan)", async () => { + globalThis.fetch = (async () => { + return new Response(JSON.stringify({ + activity: { cost: "1.68054", period: { type: "last_4_weeks" } }, + limits: { + monthly: { usage: 0.004, models: [{ name: "glm-5.3-flash", request_count: 147 }] }, + }, + }), { status: 200 }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports( + keyQuotaConfig("ollama-cloud", "https://ollama.com/v1"), + true, + ); + + expect(result.reports).toHaveLength(1); + expect(result.reports[0]?.source).toBe("ollama-cloud:usage"); + expect(result.reports[0]?.quota).toMatchObject({ + monthlyPercent: 0.4, + }); + expect(result.reports[0]?.quota.fiveHourPercent).toBeUndefined(); + expect(result.reports[0]?.quota.weeklyPercent).toBeUndefined(); + }); + + test("Ollama Cloud maps combined windows when both legacy and migrated limits exist", async () => { + globalThis.fetch = (async () => { + return new Response(JSON.stringify({ + limits: { + session: { usage: 0.1 }, + weekly: { usage: 0.2 }, + monthly: { usage: 0.3 }, + }, + }), { status: 200 }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports( + keyQuotaConfig("ollama-cloud", "https://ollama.com/v1"), + true, + ); + + expect(result.reports).toHaveLength(1); + expect(result.reports[0]?.quota).toMatchObject({ + fiveHourPercent: 10, + weeklyPercent: 20, + monthlyPercent: 30, + }); + }); + + test("Ollama Cloud treats 404 as a no-report, not terminal", async () => { + globalThis.fetch = (async () => new Response("not found", { status: 404 })) as typeof fetch; + const config = keyQuotaConfig("ollama-cloud", "https://ollama.com/v1"); + + const result = await fetchProviderQuotaReports(config, true); + expect(result.reports).toEqual([]); + }); + + test("Ollama Cloud treats 401 as terminal failure (invalid key)", async () => { + globalThis.fetch = (async () => new Response(JSON.stringify({ error: "invalid credentials" }), { status: 401 })) as typeof fetch; + const config = keyQuotaConfig("ollama-cloud", "https://ollama.com/v1"); + + const result = await fetchProviderQuotaReports(config, true); + expect(result.reports).toEqual([]); + }); + + test("Ollama Cloud never sends the key to a non-canonical base URL", async () => { + const seen: string[] = []; + globalThis.fetch = (async (input: RequestInfo | URL) => { + seen.push(String(input)); + return new Response("unexpected", { status: 500 }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports( + keyQuotaConfig("ollama-cloud", "https://attacker.example/v1"), + true, + ); + + expect(result.reports).toEqual([]); + expect(seen).toEqual([]); + }); + + test("parseOllamaCloudQuota handles edge cases", () => { + expect(parseOllamaCloudQuota(null)).toBeNull(); + expect(parseOllamaCloudQuota({})).toBeNull(); + expect(parseOllamaCloudQuota({ limits: {} })).toBeNull(); + expect(parseOllamaCloudQuota({ limits: { session: { usage: 0 } } })).toMatchObject({ + fiveHourPercent: 0, + }); + expect(parseOllamaCloudQuota({ limits: { session: { usage: 1 } } })).toMatchObject({ + fiveHourPercent: 100, + }); + expect(parseOllamaCloudQuota({ limits: { session: { usage: 1.25 } } })).toMatchObject({ + fiveHourPercent: 100, + }); + }); }); From ac922e01f5bf0495f8a62a5ce6592be5f51d6fa2 Mon Sep 17 00:00:00 2001 From: jun Date: Sat, 5 Sep 2026 11:43:14 +0900 Subject: [PATCH 2/4] fix(quota): pin the Antigravity summary probe to Google's own host Carries #3447 by @hualiny (Antigravity weekly windows via retrieveUserQuotaSummary, plus Ollama Cloud quota from /api/usage), rebased rename-aware onto tests/providers/. The provider-level probe added there sent the stored account bearer to an operator-configured baseUrl with default redirect following, while fetchAntigravityUsageQuota in the same file already pins the identical request. Route it through providerOutboundPost against ANTIGRAVITY_ACCOUNT_QUOTA_BASE with the providerRedirectError check so a configured baseUrl stays a routing choice for model requests, not a second destination for Google's accounting. The fetchAvailableModels fallback is unchanged from dev. Co-authored-by: hualiny <82697947+hualiny@users.noreply.github.com> --- src/providers/quota.ts | 11 ++- tests/providers/provider-quota.test.ts | 123 ++++++++++++++++++++++--- 2 files changed, 116 insertions(+), 18 deletions(-) diff --git a/src/providers/quota.ts b/src/providers/quota.ts index c2911a2129..d2931c59fd 100644 --- a/src/providers/quota.ts +++ b/src/providers/quota.ts @@ -2546,9 +2546,13 @@ async function fetchAntigravityQuota(provider: string, config: OcxProviderConfig } 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 fetch(`${baseUrl}/v1internal:retrieveUserQuotaSummary`, { - method: "POST", + const summaryResponse = await providerOutboundPost("google-antigravity", { baseUrl: ANTIGRAVITY_ACCOUNT_QUOTA_BASE }, summaryUrl, { headers: { Accept: "application/json", "Content-Type": "application/json", @@ -2557,7 +2561,8 @@ async function fetchAntigravityQuota(provider: string, config: OcxProviderConfig }, 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))); diff --git a/tests/providers/provider-quota.test.ts b/tests/providers/provider-quota.test.ts index 59eefeb265..051d22313f 100644 --- a/tests/providers/provider-quota.test.ts +++ b/tests/providers/provider-quota.test.ts @@ -17,6 +17,7 @@ import { parseXaiCreditsResponse, QUOTA_RESPONSE_MAX_BYTES, readProviderQuotaJsonForTests, + setAntigravityAccountQuotaTransportForTests, setProviderQuotaBeforePublishForTests, } from "../../src/providers/quota"; import type { OcxConfig } from "../../src/types"; @@ -93,6 +94,7 @@ afterEach(() => { clearAccountQuota(); clearProviderQuotaCache(); setProviderQuotaBeforePublishForTests(null); + setAntigravityAccountQuotaTransportForTests(null); if (previousOpencodexHome === undefined) delete process.env.OPENCODEX_HOME; else process.env.OPENCODEX_HOME = previousOpencodexHome; if (previousCodexHome === undefined) delete process.env.CODEX_HOME; @@ -2812,12 +2814,13 @@ describe("fetchProviderQuotaReports", () => { }); const seen: Array<{ url: string; auth?: string; body?: string }> = []; - globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { - const url = String(input); - const headers = init?.headers as Record | undefined; - seen.push({ url, auth: headers?.Authorization, body: typeof init?.body === "string" ? init.body : undefined }); - - if (url.endsWith("retrieveUserQuotaSummary")) { + globalThis.fetch = (async () => { throw new Error("plain fetch must not be used for account bearers"); }) 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, _pinned, body, _signal, requestOptions) => { + const auth = new Headers(requestOptions?.headers).get("authorization") ?? undefined; + seen.push({ url, auth, body: typeof body === "string" ? body : undefined }); + if (!url.endsWith("retrieveUserQuotaSummary")) return new Response("not found", { status: 404 }); return new Response(JSON.stringify({ groups: [ { @@ -2862,9 +2865,8 @@ describe("fetchProviderQuotaReports", () => { }, ], }), { status: 200, headers: { "content-type": "application/json" } }); - } - return new Response("not found", { status: 404 }); - }) as typeof fetch; + }, + }); const config = { defaultProvider: "google-antigravity", @@ -2901,9 +2903,11 @@ describe("fetchProviderQuotaReports", () => { projectId: "agy-nested-project", }); - globalThis.fetch = (async (input: RequestInfo | URL) => { - const url = String(input); - if (url.endsWith("retrieveUserQuotaSummary")) { + globalThis.fetch = (async () => { throw new Error("plain fetch must not be used for account bearers"); }) 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) => { + if (!url.endsWith("retrieveUserQuotaSummary")) return new Response("not found", { status: 404 }); return new Response(JSON.stringify({ groups: [ { @@ -2951,9 +2955,8 @@ describe("fetchProviderQuotaReports", () => { }, ], }), { status: 200, headers: { "content-type": "application/json" } }); - } - return new Response("not found", { status: 404 }); - }) as typeof fetch; + }, + }); const config = { defaultProvider: "google-antigravity", @@ -2977,6 +2980,96 @@ describe("fetchProviderQuotaReports", () => { ]); }); + // The provider-level Antigravity probe carries the stored account bearer. A configured + // `baseUrl` is a routing choice for model requests, not a second source of Google's + // accounting, so the summary probe stays pinned to Google's own host — the same guarantee + // `fetchAntigravityUsageQuota` already makes for the per-account path. + test("Google Antigravity does not send the account bearer to a configured baseUrl", async () => { + await saveCredential("google-antigravity", { + access: "agy-pinned-access", + refresh: "agy-pinned-refresh", + expires: Date.now() + 3600_000, + projectId: "agy-pinned-project", + }); + + const plainFetchUrls: string[] = []; + globalThis.fetch = (async (input: RequestInfo | URL) => { + plainFetchUrls.push(String(input)); + throw new Error("plain fetch must not be used for account bearers"); + }) as typeof fetch; + + const seen: Array<{ url: string; auth?: string; hostname: string }> = []; + setAntigravityAccountQuotaTransportForTests({ + resolveAddresses: async (url: string) => { + seen.push({ url, hostname: new URL(url).hostname }); + return { hostname: new URL(url).hostname, addresses: [{ address: "142.250.0.1", family: 4 }], privateNetwork: false }; + }, + pinnedPost: async (url, _pinned, _body, _signal, requestOptions) => { + seen.push({ url, auth: new Headers(requestOptions?.headers).get("authorization") ?? undefined, hostname: new URL(url).hostname }); + if (!url.endsWith("retrieveUserQuotaSummary")) return new Response("not found", { status: 404 }); + return new Response(JSON.stringify({ + groups: [{ + displayName: "Gemini Models", + buckets: [{ bucketId: "gemini-5h", window: "5h", resetTime: "2026-09-04T08:00:00Z", remainingFraction: 0.6 }], + }], + }), { status: 200, headers: { "content-type": "application/json" } }); + }, + }); + + const result = await fetchProviderQuotaReports({ + defaultProvider: "google-antigravity", + providers: { + "google-antigravity": { + adapter: "google", + authMode: "oauth", + baseUrl: "http://127.0.0.1:1/", + }, + }, + } as unknown as OcxConfig, true); + + expect(result.reports).toHaveLength(1); + expect(result.reports[0]!.source).toBe("google-antigravity:retrieveUserQuotaSummary"); + // Every bearer-carrying request went to Google's own host, not the configured loopback base. + expect(seen.map(entry => entry.hostname)).toEqual(["daily-cloudcode-pa.googleapis.com", "daily-cloudcode-pa.googleapis.com"]); + expect(seen.find(entry => entry.auth)?.url).toBe("https://daily-cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary"); + expect(seen.find(entry => entry.auth)?.auth).toBe("Bearer agy-pinned-access"); + expect(plainFetchUrls).toEqual([]); + }); + + test("Google Antigravity refuses a redirected summary response", async () => { + await saveCredential("google-antigravity", { + access: "agy-redirect-access", + refresh: "agy-redirect-refresh", + expires: Date.now() + 3600_000, + projectId: "agy-redirect-project", + }); + globalThis.fetch = (async () => { throw new Error("plain fetch must not be used for account bearers"); }) as typeof fetch; + + const posted: string[] = []; + setAntigravityAccountQuotaTransportForTests({ + resolveAddresses: async () => ({ hostname: "daily-cloudcode-pa.googleapis.com", addresses: [{ address: "142.250.0.1", family: 4 }], privateNetwork: false }), + pinnedPost: async (url) => { + posted.push(url); + return new Response(null, { status: 302, headers: { location: "http://127.0.0.1/" } }); + }, + }); + + const result = await fetchProviderQuotaReports({ + defaultProvider: "google-antigravity", + providers: { + "google-antigravity": { + adapter: "google", + authMode: "oauth", + baseUrl: "https://daily-cloudcode-pa.googleapis.com", + }, + }, + } as unknown as OcxConfig, true); + + expect(result.reports).toEqual([]); + // The redirect short-circuits the summary probe; the redirect target is never fetched. + expect(posted).toEqual(["https://daily-cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary"]); + }); + test("Ollama Cloud maps 5-hour session and weekly windows from /api/usage (legacy plan)", async () => { const seen: Array<{ url: string; authorization?: string; redirect?: RequestRedirect }> = []; globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { From 464bb27b6cb321c4aa0a266f349178f60f4815fe Mon Sep 17 00:00:00 2001 From: jun Date: Sat, 5 Sep 2026 11:43:37 +0900 Subject: [PATCH 3/4] docs(providers): state that Antigravity quota probes are pinned to Google's host Co-authored-by: hualiny <82697947+hualiny@users.noreply.github.com> --- docs-site/src/content/docs/reference/configuration/providers.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index baaf32a8e5..d7b7172dc8 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -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`. | | `location?` | `string` | Vertex location; environment fallback is `GOOGLE_CLOUD_LOCATION`. | | `mcpServers?` | `Record` | Cursor only: stdio or Streamable HTTP MCP servers. | | `desktopExecutor?` | `DesktopExecutorConfig` | Cursor only: external computer-use and record-screen commands. | From 4a721e459dc733a83fa1e513b44de7e383e6caf1 Mon Sep 17 00:00:00 2001 From: jun Date: Sat, 5 Sep 2026 11:47:28 +0900 Subject: [PATCH 4/4] test(quota): inject the pinned Antigravity transport in the multi-provider quota test After the summary probe moved off globalThis.fetch, this test made a real request to Google (sandbox DNS failure masked it as a fallthrough). Inject the seam with a 404 so the fetchAvailableModels fallback is what the test exercises, as before. Co-authored-by: hualiny <82697947+hualiny@users.noreply.github.com> --- tests/providers/provider-quota.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/providers/provider-quota.test.ts b/tests/providers/provider-quota.test.ts index 051d22313f..5a02e53f28 100644 --- a/tests/providers/provider-quota.test.ts +++ b/tests/providers/provider-quota.test.ts @@ -203,6 +203,14 @@ describe("fetchProviderQuotaReports", () => { await saveCredential("google-antigravity", { access: "agy-access-secret", refresh: "agy-refresh-secret", expires: Date.now() + 3600_000, projectId: "agy-project-secret" }); await saveCredential("kimi", { access: "kimi-access-secret", refresh: "kimi-refresh-secret", expires: Date.now() + 3600_000 }); + // The Antigravity summary probe is pinned to Google's host through the provider-outbound + // transport and never touches globalThis.fetch; without this seam the test would make a + // real network request. A 404 here exercises the fetchAvailableModels fallback below. + setAntigravityAccountQuotaTransportForTests({ + resolveAddresses: async () => ({ hostname: "daily-cloudcode-pa.googleapis.com", addresses: [{ address: "142.250.0.1", family: 4 }], privateNetwork: false }), + pinnedPost: async () => new Response("not found", { status: 404 }), + }); + const seen: { url: string; authorization?: string; body?: string }[] = []; globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { const url = String(input);