/**
* Antigravity model discovery via the Cloud Code Assist fetchAvailableModels
* endpoint. Copied from oh-my-pi's catalog discovery/antigravity module.
*/
import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
import { resolveAntigravityModelCost } from "./models.js";
import { ANTIGRAVITY_ENDPOINT_FALLBACKS, ensureAntigravityVersion, getAntigravityUserAgent } from "./wire.js";
const FETCH_AVAILABLE_MODELS_PATH = "/v1internal:fetchAvailableModels";
const DEFAULT_CONTEXT_WINDOW = 200_000;
const DEFAULT_MAX_TOKENS = 64_000;
const ANTIGRAVITY_DISCOVERY_DENYLIST = new Set(["chat_20706", "chat_23310", "gemini-2.5-pro"]);
interface AntigravityDiscoveryApiModel {
displayName?: string;
supportsImages?: boolean;
supportsThinking?: boolean;
maxTokens?: number;
maxOutputTokens?: number;
isInternal?: boolean;
}
interface AntigravityDiscoveryApiResponse {
models?: Record<string, AntigravityDiscoveryApiModel>;
}
function toPositiveNumber(value: unknown, fallback: number): number {
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
}
function parseAntigravityDiscoveryResponse(value: unknown): AntigravityDiscoveryApiResponse | null {
if (typeof value !== "object" || value === null) return null;
const payload = value as AntigravityDiscoveryApiResponse;
if (typeof payload.models !== "object" || payload.models === null) return null;
return payload;
}
function trimTrailingSlashes(value: string): string {
return value.replace(/\/+$/, "");
}
/**
* Fetch discoverable Antigravity models and normalize them into model configs.
* Returns null on network/payload/auth failures; [] only on a successful empty response.
*/
export async function fetchAntigravityModels(
token: string,
options: { endpoint?: string; signal?: AbortSignal; fetcher?: typeof fetch } = {},
): Promise<ProviderModelConfig[] | null> {
if (options.fetcher === undefined) {
await ensureAntigravityVersion(fetch, options.signal);
}
const fetcher = options.fetcher ?? fetch;
const endpoints = options.endpoint
? [trimTrailingSlashes(options.endpoint)]
: ANTIGRAVITY_ENDPOINT_FALLBACKS.map(trimTrailingSlashes);
for (const endpoint of endpoints) {
let response: Response;
try {
response = await fetcher(`${endpoint}${FETCH_AVAILABLE_MODELS_PATH}`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
"User-Agent": getAntigravityUserAgent(),
},
body: JSON.stringify({}),
signal: options.signal,
});
} catch {
continue;
}
if (!response.ok) {
continue;
}
let payload: unknown;
try {
payload = await response.json();
} catch {
continue;
}
const parsed = parseAntigravityDiscoveryResponse(payload);
if (!parsed) {
continue;
}
const models: ProviderModelConfig[] = [];
for (const [modelId, model] of Object.entries(parsed.models ?? {})) {
if (ANTIGRAVITY_DISCOVERY_DENYLIST.has(modelId)) continue;
if (model.isInternal === true) continue;
const supportsImages = model.supportsImages === true;
models.push({
id: modelId,
name: model.displayName || modelId,
reasoning: model.supportsThinking === true,
input: supportsImages ? ["text", "image"] : ["text"],
cost: resolveAntigravityModelCost(modelId),
contextWindow: toPositiveNumber(model.maxTokens, DEFAULT_CONTEXT_WINDOW),
maxTokens: toPositiveNumber(model.maxOutputTokens, DEFAULT_MAX_TOKENS),
});
}
models.sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
return models;
}
return null;
}