Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/adapters/google-antigravity-hosts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const DAILY_ANTIGRAVITY_HOST = "https://daily-cloudcode-pa.googleapis.com";
const PROD_ANTIGRAVITY_HOST = "https://cloudcode-pa.googleapis.com";

/**
* Return the configured Antigravity endpoint and, for Google's known daily/prod hosts
* only, its daily/production peer. Custom baseUrl values stay single-host.
*/
export function antigravityHostCandidates(configuredBase: string): string[] {
const configured = configuredBase.replace(/\/+$/, "");
if (configured === DAILY_ANTIGRAVITY_HOST) {
return [DAILY_ANTIGRAVITY_HOST, PROD_ANTIGRAVITY_HOST];
}
if (configured === PROD_ANTIGRAVITY_HOST) {
return [PROD_ANTIGRAVITY_HOST, DAILY_ANTIGRAVITY_HOST];
}
return [configured];
}

/** OAuth bearer requests must not use a cleartext host, even if generic baseUrl config allows http. */
export function isAntigravityHttpsHost(host: string): boolean {
try {
return new URL(host).protocol === "https:";
} catch {
return false;
}
}
7 changes: 7 additions & 0 deletions src/adapters/google-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ function googleErrorDetail(payloadText: string): { message?: string; status?: st
};
}

const ANTIGRAVITY_GEO_BLOCKED_MARKER = "user location is not supported for the api use";

export function isAntigravityGeoBlockedBody(payloadText: string): boolean {
return payloadText.toLowerCase().includes(ANTIGRAVITY_GEO_BLOCKED_MARKER);
}

function classifyGoogle(label: string, status: number | undefined, enumStatus: string | undefined, text: string): string {
const lower = `${enumStatus ?? ""} ${text}`.toLowerCase();
const quotaExhausted =
Expand All @@ -29,6 +35,7 @@ function classifyGoogle(label: string, status: number | undefined, enumStatus: s
if (status === 401 || enumStatus === "UNAUTHENTICATED" || lower.includes("unauthenticated") || lower.includes("invalid authentication") || lower.includes("expired")) {
return `${label} authentication failed`;
}
if (isAntigravityGeoBlockedBody(lower)) return `${label} location not supported`;
if (status === 403 || enumStatus === "PERMISSION_DENIED" || lower.includes("permission denied") || lower.includes("access denied")) {
return `${label} access denied`;
}
Expand Down
216 changes: 216 additions & 0 deletions src/providers/antigravity-quota.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import { antigravityUserAgent } from "../adapters/client-fingerprint";
import { antigravityHostCandidates, isAntigravityHttpsHost } from "../adapters/google-antigravity-hosts";
import { readProviderQuotaJsonForTests } from "./quota";
Comment thread
yansigit marked this conversation as resolved.
import type { ProviderQuota, ProviderQuotaWindow } from "./quota";

const LIVE_QUOTA_PATH = "/v1internal:retrieveUserQuota";
const LIVE_SUMMARY_PATH = "/v1internal:retrieveUserQuotaSummary";

type FetchImpl = typeof fetch;

export interface AntigravityLiveQuotaArgs {
accessToken: string;
projectId: string;
baseUrl: string;
timeoutMs: number;
fetchImpl?: FetchImpl;
}

interface QuotaCandidate {
record: Record<string, unknown>;
path: string[];
}

function asRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" && !Array.isArray(value)
? value as Record<string, unknown>
: null;
}

function finiteNumber(value: unknown): number | undefined {
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value === "string" && value.trim()) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : undefined;
}
return undefined;
}

function normalizePercent(value: unknown): number | undefined {
const numeric = finiteNumber(value);
return numeric === undefined ? undefined : Math.max(0, Math.min(100, numeric));
}

function resetAt(value: unknown): number | undefined {
const numeric = finiteNumber(value);
if (numeric !== undefined && numeric > 0) return numeric > 10_000_000_000 ? numeric : numeric * 1000;
if (typeof value !== "string" || !value.trim()) return undefined;
const parsed = Date.parse(value);
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
}

function remainingPercent(record: Record<string, unknown>): number | undefined {
const fraction = finiteNumber(record.remainingFraction);
if (fraction !== undefined) return normalizePercent(fraction * 100);
const percentage = finiteNumber(
record.remainingPercentage
?? record.remainingPercent
?? record.remaining_percent,
);
if (percentage !== undefined) return normalizePercent(percentage);
return undefined;
}

function usedPercent(record: Record<string, unknown>): number | undefined {
const remaining = remainingPercent(record);
return remaining === undefined ? undefined : normalizePercent(100 - remaining);
}

function recordResetAt(record: Record<string, unknown>): number | undefined {
return resetAt(record.resetTime ?? record.resetAt ?? record.resetsAt ?? record.reset_time ?? record.nextReset);
}

function collectCandidates(value: unknown, path: string[] = [], output: QuotaCandidate[] = []): QuotaCandidate[] {
if (Array.isArray(value)) {
for (const [index, item] of value.entries()) collectCandidates(item, [...path, String(index)], output);
return output;
}
const record = asRecord(value);
if (!record) return output;
output.push({ record, path });
for (const [key, child] of Object.entries(record)) {
if (child && typeof child === "object") collectCandidates(child, [...path, key], output);
}
return output;
}

function candidateModelName(record: Record<string, unknown>): string {
const explicit = record.modelId
?? record.model_id
?? record.modelName
?? record.model
?? record.name
?? record.displayName;
return typeof explicit === "string" ? explicit.toLowerCase() : "";
}

function parseGeminiWindow(payload: unknown): ProviderQuotaWindow | undefined {
for (const candidate of collectCandidates(payload)) {
if (!candidateModelName(candidate.record).includes("gemini")) continue;
const percent = usedPercent(candidate.record);
if (percent === undefined) continue;
const reset = recordResetAt(candidate.record);
return {
label: "Gem",
percent,
...(reset !== undefined ? { resetAt: reset } : {}),
};
}
return undefined;
}

function isWeeklyPath(path: string[]): boolean {
const leaf = path.at(-1);
return typeof leaf === "string" && /weekly|week|seven[_-]?day/i.test(leaf);
}

function parseWeeklyWindow(payload: unknown): { percent: number; resetAt?: number } | undefined {
const candidates = collectCandidates(payload);
for (const candidate of candidates) {
if (!isWeeklyPath(candidate.path)) continue;
const percent = usedPercent(candidate.record);
if (percent === undefined) continue;
const reset = recordResetAt(candidate.record);
return { percent, ...(reset !== undefined ? { resetAt: reset } : {}) };
}
return undefined;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

async function readJson(response: Response, timeoutMs: number): Promise<unknown> {
const payload = await readProviderQuotaJsonForTests(response, timeoutMs);
if (payload === null) throw new Error("Antigravity quota RPC returned unreadable JSON");
return payload;
}

export class AntigravityQuotaRpcError extends Error {
constructor(readonly status: number) {
super(`Antigravity quota RPC failed: ${status}`);
}
}

export function isTerminalAntigravityQuotaStatus(status: number): boolean {
return status === 401 || status === 403 || status === 429;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function terminalRpcError(result: PromiseSettledResult<unknown>): AntigravityQuotaRpcError | null {
if (result.status !== "rejected" || !(result.reason instanceof AntigravityQuotaRpcError)) return null;
return isTerminalAntigravityQuotaStatus(result.reason.status) ? result.reason : null;
}

async function fetchRpc(
fetchImpl: FetchImpl,
host: string,
method: "retrieveUserQuota" | "retrieveUserQuotaSummary",
args: AntigravityLiveQuotaArgs,
): Promise<unknown> {
const path = method === "retrieveUserQuota" ? LIVE_QUOTA_PATH : LIVE_SUMMARY_PATH;
const response = await fetchImpl(`${host}${path}`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent": antigravityUserAgent(),
Authorization: `Bearer ${args.accessToken}`,
},
body: JSON.stringify({ project: args.projectId }),
redirect: "error",
signal: AbortSignal.timeout(args.timeoutMs),
});
if (!response.ok) throw new AntigravityQuotaRpcError(response.status);
return readJson(response, args.timeoutMs);
}

async function fetchHostQuota(
fetchImpl: FetchImpl,
host: string,
args: AntigravityLiveQuotaArgs,
): Promise<ProviderQuota | null> {
const [quotaResult, summaryResult] = await Promise.allSettled([
fetchRpc(fetchImpl, host, "retrieveUserQuota", args),
fetchRpc(fetchImpl, host, "retrieveUserQuotaSummary", args),
]);
const terminalError = terminalRpcError(quotaResult) ?? terminalRpcError(summaryResult);
if (terminalError) throw terminalError;
if (quotaResult.status === "rejected") return null;
const quotaPayload = quotaResult.value;
const summaryPayload = summaryResult.status === "fulfilled" ? summaryResult.value : null;
const gem = parseGeminiWindow(quotaPayload);
const weekly = parseWeeklyWindow(summaryPayload);
if (!gem && !weekly) return null;
return {
...(gem ? { customWindows: [gem] } : {}),
...(weekly ? {
weeklyPercent: weekly.percent,
...(weekly.resetAt !== undefined ? { weeklyResetAt: weekly.resetAt } : {}),
} : {}),
updatedAt: Date.now(),
};
}

export async function fetchAntigravityLiveQuota(
args: AntigravityLiveQuotaArgs,
): Promise<ProviderQuota | null> {
const fetchImpl = args.fetchImpl ?? fetch;
for (const host of antigravityHostCandidates(args.baseUrl)) {
if (!isAntigravityHttpsHost(host)) continue;
try {
const quota = await fetchHostQuota(fetchImpl, host, args);
if (quota) return quota;
} catch (error) {
if (error instanceof AntigravityQuotaRpcError && isTerminalAntigravityQuotaStatus(error.status)) {
throw error;
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
return null;
}
Loading
Loading