import { createHash } from "node:crypto"; import { Platform } from "../types/enums.js"; import { getCurrentAvailableModels, getCurrentEndpoint, getCurrentMatchedModelDebug, setLastAvailableModels, setLastEndpoint, setLastError, setLastMatchedModelDebug, setLastStatus, } from "../diagnostics/diagnostics.js"; import { assertSafeApiBaseUrl, safeError } from "../utils/security.js"; import type { AntigravityApiKey, DynamicModelInfo } from "../types/types.js"; import { antigravityEnv, asString, escapeRegExp, isRecord } from "../utils/util.js"; import { antigravityFetch } from "../utils/http.js"; export const DEFAULT_ENDPOINT = "https://cloudcode-pa.googleapis.com"; export const ENDPOINT_FALLBACKS = [ DEFAULT_ENDPOINT, "https://daily-cloudcode-pa.sandbox.googleapis.com", ]; const PROJECT_CACHE_TTL_MS = 30 * 60 * 1000; const projectCache = new Map(); const MODEL_CACHE_TTL_MS = 30 * 60 * 1000; const modelCache = new Map(); /** Metadata lookups (project/model discovery) must be fast; a stalled endpoint should * fall through to the next candidate instead of hanging the whole request. */ const DISCOVERY_TIMEOUT_MS = 8000; /** In-flight de-dupe: concurrent requests for the same (token, project, model) share one probe. */ const inFlightModelLookups = new Map>(); /** UUID-shaped stable id from a seed (account email preferred over cwd). */ export function stableProjectId(seed: string): string { const bytes = createHash("sha1").update(`antigravity:${seed}`).digest().subarray(0, 16); bytes[6] = (bytes[6] & 0x0f) | 0x50; bytes[8] = (bytes[8] & 0x3f) | 0x80; const hex = bytes.toString("hex"); return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; } /** * Fallback project id when discovery fails. * Prefer ANTIGRAVITY_PROJECT_ID, then a stable seed (email), never process.cwd(). */ export function defaultProjectId(seed = "antigravity-default"): string { return antigravityEnv("PROJECT_ID")?.trim() || stableProjectId(seed); } /** @deprecated Use defaultProjectId(seed); kept for scripts that imported the old constant. */ export const DEFAULT_PROJECT_ID = defaultProjectId(); export function endpointCandidates(): string[] { const explicit = antigravityEnv("BASE_URL")?.trim(); return explicit ? [assertSafeApiBaseUrl(explicit)] : ENDPOINT_FALLBACKS; } function defaultUserAgent(): string { const os = process.platform === "darwin" ? "darwin" : process.platform === "win32" ? "windows" : "linux"; const arch = process.arch === "x64" ? "amd64" : process.arch; return `antigravity/1.15.8 ${os}/${arch}`; } export function antigravityHeaders(token: string): Record { const platform = process.platform === "darwin" ? Platform.Macos : process.platform === "win32" ? Platform.Windows : Platform.Linux; return { Authorization: `Bearer ${token}`, "Content-Type": "application/json", Accept: "text/event-stream", "User-Agent": antigravityEnv("USER_AGENT") || defaultUserAgent(), "X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1", "Client-Metadata": JSON.stringify({ ideType: "ANTIGRAVITY", platform, pluginType: "GEMINI", }), }; } export function jsonOrTextError(text: string): string { try { const parsed = JSON.parse(text) as { error?: { message?: string; status?: string; code?: number }; }; if (parsed.error?.message) return parsed.error.message; } catch { // not JSON } return text; } export function parseApiKey(apiKeyRaw: string | undefined): AntigravityApiKey { if (!apiKeyRaw) { throw new Error("No Antigravity OAuth credentials. Run /login antigravity."); } try { const parsed = JSON.parse(apiKeyRaw) as Partial; if (!parsed.token || !parsed.projectId) throw new Error("missing token or projectId"); return { token: parsed.token, projectId: parsed.projectId }; } catch (error) { throw new Error( `Invalid Antigravity credentials. Run /login antigravity. (${safeError(error)})`, { cause: error }, ); } } export function extractProjectId(data: unknown): string | undefined { if (!isRecord(data)) return undefined; const direct = data.antigravityProjectId ?? data.projectId ?? data.backendProjectId ?? data.userDefinedCloudaicompanionProject ?? data.cloudaicompanionProject ?? data.project; const directId = asString(direct); if (directId) return directId; if (isRecord(direct)) { const nestedId = asString(direct.id); if (nestedId) return nestedId; } for (const key of ["projects", "projectIds", "cloudaicompanionProjects"]) { const value = data[key]; if (Array.isArray(value)) { for (const item of value) { const nested = extractProjectId(item); if (nested) return nested; const itemId = asString(item); if (itemId) return itemId; } } } return undefined; } async function listCloudAICompanionProjects(token: string): Promise { for (const endpoint of endpointCandidates()) { try { const res = await antigravityFetch(`${endpoint}/v1internal:listCloudAICompanionProjects`, { method: "POST", headers: antigravityHeaders(token), body: JSON.stringify({}), signal: AbortSignal.timeout(DISCOVERY_TIMEOUT_MS), }); setLastStatus(res.status); setLastEndpoint(endpoint); if (!res.ok) continue; return extractProjectId(await res.json()); } catch (error) { setLastError(safeError(error)); } } return undefined; } function collectModelLabels(value: unknown, out: string[] = []): string[] { if (!value || out.length > 50) return out; if (typeof value === "string") { if (/gemini|claude|gpt-oss/i.test(value)) out.push(value); return out; } if (Array.isArray(value)) { for (const item of value) collectModelLabels(item, out); return out; } if (isRecord(value)) { for (const key of ["id", "name", "label", "displayName", "model", "modelId"]) { collectModelLabels(value[key], out); } for (const nested of Object.values(value)) { if (nested && typeof nested === "object") collectModelLabels(nested, out); } } return out; } function summarizeModelCandidate(value: unknown): string { if (!isRecord(value)) return String(value ?? "none"); const out: Record = {}; for (const [key, raw] of Object.entries(value)) { if (/token|auth|credential|secret|email/i.test(key)) continue; if (raw === null || ["string", "number", "boolean"].includes(typeof raw)) out[key] = raw; else if (Array.isArray(raw)) out[key] = `[array:${String(raw.length)}]`; else if (isRecord(raw)) { out[key] = `{${Object.keys(raw).slice(0, 12).join(",")}}`; } } return JSON.stringify(out).slice(0, 1200); } /** Runtime ids look like gemini-*, claude-*, gpt-oss-*, never MODEL_PLACEHOLDER_* enums. */ export function isUsableRuntimeModelId(id: string): boolean { return /^(gemini-|claude-|gpt-oss-)/i.test(id) && !/\s/.test(id) && !/^MODEL_/i.test(id); } function buildModelMatchRegex(requestedId: string): RegExp { const req = requestedId.toLowerCase(); // Display names from fetchAvailableModels (keys are the real runtime ids): // gemini-3.7-flash-low → "Gemini 3.7 Flash (Low)" // gemini-3.7-flash-medium → "Gemini 3.7 Flash (Medium)" // gemini-3.7-flash-high → "Gemini 3.7 Flash (High)" // gemini-3.6-flash-low → "Gemini 3.6 Flash (Low)" // gemini-3.6-flash-medium → "Gemini 3.6 Flash (Medium)" // gemini-3.6-flash-high → "Gemini 3.6 Flash (High)" // gemini-3.5-flash-extra-low → "Gemini 3.5 Flash (Low)" // gemini-3.5-flash-low → "Gemini 3.5 Flash (Medium)" // gemini-3-flash-agent → "Gemini 3.5 Flash (High)" if (req === "gemini-3.7-flash-low") return /gemini[- ]3\.7[- ]flash \(low\)/i; if (req === "gemini-3.7-flash-medium") return /gemini[- ]3\.7[- ]flash \(medium\)/i; if (req === "gemini-3.7-flash-high") return /gemini[- ]3\.7[- ]flash \(high\)/i; if (req === "gemini-3.6-flash-low") return /gemini[- ]3\.6[- ]flash \(low\)/i; if (req === "gemini-3.6-flash-medium") return /gemini[- ]3\.6[- ]flash \(medium\)/i; if (req === "gemini-3.6-flash-high") return /gemini[- ]3\.6[- ]flash \(high\)/i; if (req === "gemini-3.5-flash-extra-low") return /gemini[- ]3\.5[- ]flash \(low\)/i; if (req === "gemini-3.5-flash-low" || req === "gemini-3.5-flash-medium") return /gemini[- ]3\.5[- ]flash \(medium\)/i; if (req === "gemini-3.5-flash-high" || req === "gemini-3-flash-agent") return /gemini[- ]3\.5[- ]flash \(high\)/i; if (req.includes("claude-opus-4-6")) return /claude.*opus.*4\.6/i; if (req.includes("claude-sonnet-4-6")) return /claude.*sonnet.*4\.6/i; if (req.includes("gpt-oss-120b")) return /gpt.*oss.*120b/i; if (req === "gemini-3.1-pro-low") return /gemini[- ]3\.1[- ]pro \(low\)/i; if (req === "gemini-3.1-pro-high" || req === "gemini-pro-agent") return /gemini[- ]3\.1[- ]pro \(high\)/i; const escaped = escapeRegExp(req).replace(/\\-/g, "[- ]"); return new RegExp(escaped, "i"); } function dynamicModelFromInfo(modelId: string, info: unknown): DynamicModelInfo { if (!isRecord(info)) return { id: modelId }; setLastMatchedModelDebug(summarizeModelCandidate({ modelId, ...info })); const experiments = Array.isArray(info.modelExperiments) ? info.modelExperiments.filter((item): item is string => typeof item === "string") : undefined; return { id: modelId, experiments, apiProvider: asString(info.apiProvider), modelProvider: asString(info.modelProvider), }; } /** * Resolve a requested runtime model against fetchAvailableModels payload. * The real runtime ids are the keys of `data.models`; the nested `model` field is often * a MODEL_PLACEHOLDER_* enum that 404s on streamGenerateContent. */ function findDynamicModel(value: unknown, requestedId: string): DynamicModelInfo | undefined { if (!value) return undefined; if (isRecord(value) && isRecord(value.models)) { const modelsMap = value.models; if (isUsableRuntimeModelId(requestedId) && requestedId in modelsMap) { return dynamicModelFromInfo(requestedId, modelsMap[requestedId]); } const targetRegex = buildModelMatchRegex(requestedId); for (const [modelId, info] of Object.entries(modelsMap)) { if (!isUsableRuntimeModelId(modelId)) continue; if (targetRegex.test(modelId)) return dynamicModelFromInfo(modelId, info); if (isRecord(info)) { const label = info.label ?? info.displayName ?? info.name; if (typeof label === "string" && targetRegex.test(label)) { return dynamicModelFromInfo(modelId, info); } } } return undefined; } const targetRegex = buildModelMatchRegex(requestedId); if (typeof value === "string") { return targetRegex.test(value) && isUsableRuntimeModelId(value) ? { id: value } : undefined; } if (Array.isArray(value)) { for (const item of value) { const found = findDynamicModel(item, requestedId); if (found) return found; } return undefined; } if (isRecord(value)) { for (const nested of Object.values(value)) { if (nested && typeof nested === "object") { const found = findDynamicModel(nested, requestedId); if (found) return found; } } } return undefined; } async function fetchAvailableRuntimeModelUncached( token: string, projectId: string, requestedRuntimeModel: string, ): Promise { const body = JSON.stringify({ project: projectId }); const endpoints = endpointCandidates(); let lastLabels = ""; // Try endpoints in priority order (production first). If the primary endpoint // resolves the model, return immediately without waiting on slower sandbox endpoints. for (const endpoint of endpoints) { try { const res = await antigravityFetch(`${endpoint}/v1internal:fetchAvailableModels`, { method: "POST", headers: antigravityHeaders(token), body, signal: AbortSignal.timeout(DISCOVERY_TIMEOUT_MS), }); setLastStatus(res.status); if (!res.ok) continue; setLastEndpoint(endpoint); const data: unknown = await res.json(); const labels = [...new Set(collectModelLabels(data))].slice(0, 16); if (labels.length) lastLabels = labels.join(","); const found = findDynamicModel(data, requestedRuntimeModel); if (found) { if (lastLabels) setLastAvailableModels(lastLabels); return found; } } catch (error) { setLastError(safeError(error)); } } if (lastLabels) setLastAvailableModels(lastLabels); return undefined; } export async function fetchAvailableRuntimeModel( token: string, projectId: string, requestedRuntimeModel: string, ): Promise { const cacheKey = `${token}::${projectId}::${requestedRuntimeModel}`; const cached = modelCache.get(cacheKey); if (cached && cached.expiresAt > Date.now()) return cached.result; // De-dupe concurrent lookups for the same key (e.g. parallel requests right after // startup) so they share one probe instead of each firing their own round-trips. const inFlight = inFlightModelLookups.get(cacheKey); if (inFlight) return inFlight; const promise = fetchAvailableRuntimeModelUncached(token, projectId, requestedRuntimeModel).then( (result) => { modelCache.set(cacheKey, { result, expiresAt: Date.now() + MODEL_CACHE_TTL_MS }); return result; }, ); inFlightModelLookups.set(cacheKey, promise); try { return await promise; } finally { inFlightModelLookups.delete(cacheKey); // Evict expired entries; bound map size to avoid unbounded growth. if (modelCache.size > 64) { const now = Date.now(); for (const [key, entry] of modelCache) { if (entry.expiresAt <= now) modelCache.delete(key); } } } } export function clearModelCache(): void { modelCache.clear(); } async function loadCodeAssistUncached(token: string): Promise { const body = JSON.stringify({ metadata: { ideType: "ANTIGRAVITY", platform: "PLATFORM_UNSPECIFIED", pluginType: "GEMINI", }, }); for (const endpoint of endpointCandidates()) { try { const res = await antigravityFetch(`${endpoint}/v1internal:loadCodeAssist`, { method: "POST", headers: antigravityHeaders(token), body, signal: AbortSignal.timeout(DISCOVERY_TIMEOUT_MS), }); setLastStatus(res.status); setLastEndpoint(endpoint); if (!res.ok) continue; const project = extractProjectId(await res.json()); if (project) return project; return await listCloudAICompanionProjects(token); } catch (error) { setLastError(safeError(error)); } } return undefined; } /** Discover project id with a short in-memory LRU cache keyed by access token. */ export async function loadCodeAssist(token: string): Promise { const cached = projectCache.get(token); if (cached && cached.expiresAt > Date.now()) { // Refresh LRU order: delete and re-insert so newest is at the end. projectCache.delete(token); projectCache.set(token, cached); return cached.projectId; } const projectId = await loadCodeAssistUncached(token); projectCache.set(token, { projectId, expiresAt: Date.now() + PROJECT_CACHE_TTL_MS }); // Evict oldest entry when the cache exceeds 32 entries (O(1) — Map preserves insertion order). if (projectCache.size > 32) { const oldestKey = projectCache.keys().next().value; if (oldestKey !== undefined) projectCache.delete(oldestKey); } return projectId; } export function clearProjectCache(): void { projectCache.clear(); } export function resolveProjectId(opts: { token: string; credentialProjectId?: string; email?: string; warmedProject?: string | null; }): string { return ( antigravityEnv("PROJECT_ID")?.trim() || opts.warmedProject || opts.credentialProjectId || defaultProjectId(opts.email || "antigravity-default") ); } /** Build a diagnostic suffix using the active request bag. */ export function formatRequestDiagnostics(extra: { projectId: string; runtimeModel: string; }): string { return `endpoint=${getCurrentEndpoint() || "unknown"}, project=${extra.projectId}, runtimeModel=${extra.runtimeModel}, matched=${getCurrentMatchedModelDebug() || "none"}, available=${getCurrentAvailableModels() || "unknown"}`; }