feat: google antigravity oauth and streaming provider extension
c676bb71 +6497
1234node_modules/dist/*.log.DS_Store1234567891011121314151617181920212223242526272829# Google Antigravity provider
Login + streaming provider for Google Cloud Code Assist ("Antigravity"). Portedfrom oh-my-pi's `google-antigravity` implementation, including:
- OAuth flow with tier eligibility check and free-tier provisioning- `antigravity/hub/<version>` User-Agent spoofing with live version discovery- The per-request Antigravity envelope (`agent/<agentId>/<ts>/<trajectoryId>/<step>`, signed-decimal session id, `model_enum` / `used_claude` labels)- `daily-cloudcode-pa.googleapis.com` with sandbox failover- Live model discovery via `fetchAvailableModels`
## Usage
```bashprime-agent -e ./packages/coding-agent/examples/extensions/google-antigravity# then:/login google-antigravity```
For auto-discovery, copy this directory to `~/.prime/agent/extensions/`.
## Overrides
| Env var | Purpose ||---------|---------|| `PI_AI_ANTIGRAVITY_VERSION` | Pin the spoofed `antigravity/hub` client version || `PI_AI_ANTIGRAVITY_CL` | Pin the `cl` changelist component || `PI_AI_ANTIGRAVITY_OS` / `PI_AI_ANTIGRAVITY_ARCH` | Pin the `os_type` / `arch` components |123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113/** * 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 { 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: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, 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;}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427/** * Google Cloud Code Assist / Antigravity shared helpers. * Adapted from oh-my-pi's google-gemini-cli + google-shared provider code for * use inside a self-contained Prime extension. */import type { Api, AssistantMessage, Context, ImageContent, Message, Model, SimpleStreamOptions, StopReason, StreamOptions, TextContent, ThinkingLevel, Tool, ToolCall, ToolResultMessage,} from "@earendil-works/pi-ai";import { normalizeSchemaForCCA } from "./schema/normalize.js";
export type GoogleThinkingLevel = "THINKING_LEVEL_UNSPECIFIED" | "MINIMAL" | "LOW" | "MEDIUM" | "HIGH";
export type FunctionCallingConfigMode = "AUTO" | "ANY" | "NONE" | "VALIDATED";
export interface GooglePart { text?: string; thought?: boolean; thoughtSignature?: string; functionCall?: { name: string; args: Record<string, unknown>; id?: string }; functionResponse?: { name: string; response: unknown; parts?: GooglePart[]; id?: string }; inlineData?: { mimeType: string; data: string };}
export interface GoogleContent { role: "user" | "model"; parts: GooglePart[];}
export interface GoogleThinkingConfig { includeThoughts?: boolean; thinkingLevel?: GoogleThinkingLevel; thinkingBudget?: number;}
/** Removes unpaired Unicode surrogate characters from a string. */export function sanitizeSurrogates(text: string): string { return text.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "");}
export function headersToRecord(headers: Headers): Record<string, string> { const result: Record<string, string> = {}; for (const [key, value] of headers.entries()) { result[key] = value; } return result;}
export function buildBaseOptions(model: Model<Api>, options?: SimpleStreamOptions, apiKey?: string): StreamOptions { return { temperature: options?.temperature, maxTokens: options?.maxTokens ?? (model.maxTokens > 0 ? model.maxTokens : undefined), signal: options?.signal, apiKey: apiKey || options?.apiKey, transport: options?.transport, serviceTier: options?.serviceTier, cacheRetention: options?.cacheRetention, sessionId: options?.sessionId, headers: options?.headers, onPayload: options?.onPayload, onResponse: options?.onResponse, timeoutMs: options?.timeoutMs, maxRetries: options?.maxRetries, maxRetryDelayMs: options?.maxRetryDelayMs, metadata: options?.metadata, };}
export function clampReasoning(effort: ThinkingLevel | undefined): Exclude<ThinkingLevel, "xhigh" | "max"> | undefined { return effort === "xhigh" || effort === "max" ? "high" : effort;}
export function isThinkingPart(part: Pick<GooglePart, "thought" | "thoughtSignature">): boolean { return part.thought === true;}
export function retainThoughtSignature(existing: string | undefined, incoming: string | undefined): string | undefined { if (typeof incoming === "string" && incoming.length > 0) return incoming; return existing;}
/** Whether this Google API model requires tool-call IDs on function calls and responses. */export function isClaudeModel(modelId: string): boolean { return modelId.toLowerCase().includes("claude");}
export function requiresToolCallId(modelId: string): boolean { return modelId.startsWith("claude-") || modelId.startsWith("gpt-oss-");}
function getGeminiMajorVersion(modelId: string): number | undefined { const match = modelId.toLowerCase().match(/^gemini(?:-live)?-(\d+)/); if (!match) return undefined; return Number.parseInt(match[1], 10);}
function supportsMultimodalFunctionResponse(modelId: string): boolean { const geminiMajorVersion = getGeminiMajorVersion(modelId); if (geminiMajorVersion !== undefined) { return geminiMajorVersion >= 3; } return true;}
const base64SignaturePattern = /^[A-Za-z0-9+/]+={0,2}$/;
function isValidThoughtSignature(signature: string | undefined): boolean { if (!signature) return false; if (signature.length % 4 !== 0) return false; return base64SignaturePattern.test(signature);}
function resolveThoughtSignature(isSameProviderAndModel: boolean, signature: string | undefined): string | undefined { return isSameProviderAndModel && isValidThoughtSignature(signature) ? signature : undefined;}
const NON_VISION_USER_IMAGE_PLACEHOLDER = "(image omitted: model does not support images)";const NON_VISION_TOOL_IMAGE_PLACEHOLDER = "(tool image omitted: model does not support images)";
function replaceImagesWithPlaceholder(content: (TextContent | ImageContent)[], placeholder: string): TextContent[] { const result: TextContent[] = []; let previousWasPlaceholder = false;
for (const block of content) { if (block.type === "image") { if (!previousWasPlaceholder) { result.push({ type: "text", text: placeholder }); } previousWasPlaceholder = true; continue; } result.push(block); previousWasPlaceholder = block.text === placeholder; }
return result;}
function downgradeUnsupportedImages<TApi extends Api>(messages: Message[], model: Model<TApi>): Message[] { if (model.input.includes("image")) { return messages; }
return messages.map((msg) => { if (msg.role === "user" && Array.isArray(msg.content)) { return { ...msg, content: replaceImagesWithPlaceholder(msg.content, NON_VISION_USER_IMAGE_PLACEHOLDER) }; } if (msg.role === "toolResult") { return { ...msg, content: replaceImagesWithPlaceholder(msg.content, NON_VISION_TOOL_IMAGE_PLACEHOLDER) }; } return msg; });}
export function transformMessages<TApi extends Api>( messages: Message[], model: Model<TApi>, normalizeToolCallId?: (id: string, model: Model<TApi>, source: AssistantMessage) => string,): Message[] { const toolCallIdMap = new Map<string, string>(); const imageAwareMessages = downgradeUnsupportedImages(messages, model);
const transformed = imageAwareMessages.map((msg) => { if (msg.role === "user") { return msg; } if (msg.role === "toolResult") { const normalizedId = toolCallIdMap.get(msg.toolCallId); if (normalizedId && normalizedId !== msg.toolCallId) { return { ...msg, toolCallId: normalizedId }; } return msg; } if (msg.role === "assistant") { const assistantMsg = msg as AssistantMessage; const isSameModel = assistantMsg.provider === model.provider && assistantMsg.api === model.api && assistantMsg.model === model.id;
const transformedContent = assistantMsg.content.flatMap((block) => { if (block.type === "thinking") { if (block.redacted) { return isSameModel ? block : []; } if (isSameModel && block.thinkingSignature) return block; if (!block.thinking || block.thinking.trim() === "") return []; if (isSameModel) return block; return { type: "text" as const, text: block.thinking }; } if (block.type === "text") { if (isSameModel) return block; return { type: "text" as const, text: block.text }; } if (block.type === "toolCall") { const toolCall = block as ToolCall; let normalizedToolCall: ToolCall = toolCall; if (!isSameModel && toolCall.thoughtSignature) { normalizedToolCall = { ...toolCall }; delete (normalizedToolCall as { thoughtSignature?: string }).thoughtSignature; } if (!isSameModel && normalizeToolCallId) { const normalizedId = normalizeToolCallId(toolCall.id, model, assistantMsg); if (normalizedId !== toolCall.id) { toolCallIdMap.set(toolCall.id, normalizedId); normalizedToolCall = { ...normalizedToolCall, id: normalizedId }; } } return normalizedToolCall; } return block; });
return { ...assistantMsg, content: transformedContent }; } return msg; });
const result: Message[] = []; let pendingToolCalls: ToolCall[] = []; let existingToolResultIds = new Set<string>(); const insertSyntheticToolResults = () => { if (pendingToolCalls.length > 0) { for (const tc of pendingToolCalls) { if (!existingToolResultIds.has(tc.id)) { result.push({ role: "toolResult", toolCallId: tc.id, toolName: tc.name, content: [{ type: "text", text: "No result provided" }], isError: true, timestamp: Date.now(), } as ToolResultMessage); } } pendingToolCalls = []; existingToolResultIds = new Set(); } };
for (let i = 0; i < transformed.length; i++) { const msg = transformed[i]; if (msg.role === "assistant") { insertSyntheticToolResults(); const assistantMsg = msg as AssistantMessage; if (assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") { continue; } const toolCalls = assistantMsg.content.filter((b) => b.type === "toolCall") as ToolCall[]; if (toolCalls.length > 0) { pendingToolCalls = toolCalls; existingToolResultIds = new Set(); } result.push(msg); } else if (msg.role === "toolResult") { existingToolResultIds.add(msg.toolCallId); result.push(msg); } else if (msg.role === "user") { insertSyntheticToolResults(); result.push(msg); } else { result.push(msg); } }
insertSyntheticToolResults(); return result;}
/** Converts internal context to Google `Content[]`. */export function convertMessages(model: Model<Api>, context: Context): GoogleContent[] { const contents: GoogleContent[] = []; const normalizeToolCallId = (id: string): string => { if (!requiresToolCallId(model.id)) return id; return id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64); };
const transformedMessages = transformMessages(context.messages, model, normalizeToolCallId);
for (const msg of transformedMessages) { if (msg.role === "user") { if (typeof msg.content === "string") { contents.push({ role: "user", parts: [{ text: sanitizeSurrogates(msg.content) }] }); } else { const parts: GooglePart[] = msg.content.map((item) => { if (item.type === "text") { return { text: sanitizeSurrogates(item.text) }; } return { inlineData: { mimeType: item.mimeType, data: item.data } }; }); if (parts.length === 0) continue; contents.push({ role: "user", parts }); } } else if (msg.role === "assistant") { const parts: GooglePart[] = []; const isSameProviderAndModel = msg.provider === model.provider && msg.model === model.id;
for (const block of msg.content) { if (block.type === "text") { if (!block.text || block.text.trim() === "") continue; const thoughtSignature = resolveThoughtSignature(isSameProviderAndModel, block.textSignature); parts.push({ text: sanitizeSurrogates(block.text), ...(thoughtSignature && { thoughtSignature }) }); } else if (block.type === "thinking") { if (!block.thinking || block.thinking.trim() === "") continue; const thoughtSignature = resolveThoughtSignature(isSameProviderAndModel, block.thinkingSignature); // Claude on Antigravity Cloud Code Assist rejects unsigned thinking parts; drop them. if (isClaudeModel(model.id) && !thoughtSignature) continue; if (thoughtSignature) { parts.push({ thought: true, text: sanitizeSurrogates(block.thinking), thoughtSignature, }); } else { parts.push({ text: sanitizeSurrogates(block.thinking) }); } } else if (block.type === "toolCall") { const thoughtSignature = resolveThoughtSignature(isSameProviderAndModel, block.thoughtSignature); parts.push({ functionCall: { name: block.name, args: block.arguments ?? {}, ...(requiresToolCallId(model.id) ? { id: block.id } : {}), }, ...(thoughtSignature && { thoughtSignature }), }); } }
if (parts.length === 0) continue; contents.push({ role: "model", parts }); } else if (msg.role === "toolResult") { const textContent = msg.content.filter((c): c is TextContent => c.type === "text"); const textResult = textContent.map((c) => c.text).join("\n"); const imageContent = model.input.includes("image") ? msg.content.filter((c): c is ImageContent => c.type === "image") : [];
const hasText = textResult.length > 0; const hasImages = imageContent.length > 0; const modelSupportsMultimodalFunctionResponse = supportsMultimodalFunctionResponse(model.id);
const responseValue = hasText ? sanitizeSurrogates(textResult) : hasImages ? "(see attached image)" : "";
const imageParts: GooglePart[] = imageContent.map((imageBlock) => ({ inlineData: { mimeType: imageBlock.mimeType, data: imageBlock.data }, }));
const includeId = requiresToolCallId(model.id); const functionResponsePart: GooglePart = { functionResponse: { name: msg.toolName, response: msg.isError ? { error: responseValue } : { output: responseValue }, ...(hasImages && modelSupportsMultimodalFunctionResponse && { parts: imageParts }), ...(includeId ? { id: msg.toolCallId } : {}), }, };
const lastContent = contents[contents.length - 1]; if (lastContent?.role === "user" && lastContent.parts?.some((p) => p.functionResponse)) { lastContent.parts.push(functionResponsePart); } else { contents.push({ role: "user", parts: [functionResponsePart] }); }
if (hasImages && !modelSupportsMultimodalFunctionResponse) { contents.push({ role: "user", parts: [{ text: "Tool result image:" }, ...imageParts] }); } } }
return contents;}
export function convertTools( tools: Tool[], useParameters = false,): { functionDeclarations: Record<string, unknown>[] }[] | undefined { if (tools.length === 0) return undefined; return [ { functionDeclarations: tools.map((tool) => ({ name: tool.name, description: tool.description, ...(useParameters ? { parameters: normalizeSchemaForCCA(tool.parameters as unknown) } : { parametersJsonSchema: tool.parameters }), })), }, ];}
export function mapToolChoice(choice: string): FunctionCallingConfigMode { switch (choice) { case "auto": return "AUTO"; case "none": return "NONE"; case "any": return "ANY"; default: return "AUTO"; }}
export function mapStopReasonString(reason: string): StopReason { switch (reason) { case "STOP": return "stop"; case "MAX_TOKENS": return "length"; default: return "error"; }}12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182/** * Google Antigravity provider extension. * * Provides Google Cloud Code Assist (Antigravity) login, live model discovery, * and streaming. The wire protocol, OAuth flow, header spoofing, and request * envelope mirror oh-my-pi's google-antigravity implementation. * * Usage: * prime-agent -e ./packages/coding-agent/examples/extensions/google-antigravity * # Then /login google-antigravity * * Or drop the directory into ~/.prime/agent/extensions/ for auto-discovery. */import { readFileSync } from "node:fs";import { homedir } from "node:os";import { join } from "node:path";import type { Api, AssistantMessageEventStream, Context, Model, SimpleStreamOptions } from "@earendil-works/pi-ai";import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";import { fetchAntigravityModels } from "./discovery.js";import { STATIC_PROVIDER_MODELS } from "./models.js";import { antigravityOAuth } from "./oauth.js";import { streamSimpleAntigravity } from "./stream.js";import { ANTIGRAVITY_DAILY_ENDPOINT } from "./wire.js";
export const PROVIDER_ID = "google-antigravity";export const PROVIDER_API = "google-antigravity" as Api;
/** * Mirror prime's getAgentDir() without a runtime import of * @earendil-works/pi-coding-agent (its dist barrel pulls pi-ai/mcp through * jiti and fails to load inside an extension). */function getAgentDir(): string { const envDir = process.env.PRIME_AGENT_CODING_AGENT_DIR ?? process.env.PI_CODING_AGENT_DIR; if (envDir) { if (envDir === "~") return homedir(); if (envDir.startsWith("~/")) return join(homedir(), envDir.slice(2)); return envDir; } return join(homedir(), ".prime", "agent");}
function readExistingAccessToken(): string | undefined { try { const authPath = join(getAgentDir(), "auth.json"); const auth = JSON.parse(readFileSync(authPath, "utf-8")) as Record<string, { type?: string; access?: string }>; const entry = auth[PROVIDER_ID]; if (entry?.type === "oauth" && typeof entry.access === "string" && entry.access.length > 0) { return entry.access; } } catch { // No auth file or not logged in yet. } return undefined;}
export default async function (pi: ExtensionAPI) { const baseUrl = ANTIGRAVITY_DAILY_ENDPOINT; let models = STATIC_PROVIDER_MODELS;
const accessToken = readExistingAccessToken(); if (accessToken) { try { const discovered = await fetchAntigravityModels(accessToken); if (discovered && discovered.length > 0) { models = discovered; } } catch { // Keep the static fallback list. } }
pi.registerProvider(PROVIDER_ID, { name: "Google Antigravity", baseUrl, api: PROVIDER_API, models, oauth: antigravityOAuth, streamSimple: (model: Model<Api>, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream => streamSimpleAntigravity(model, context, options), });}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366/** * Antigravity model catalog: static fallback list plus per-model wire profiles * and thinking metadata, copied from oh-my-pi's google-antigravity catalog. * * The extension registers wire ids directly (there is no effort-tier routing * layer in Prime's extension API), so each entry below is a concrete Cloud * Code Assist wire model id. Live discovery (discovery.ts) replaces the static * list when it succeeds. */
import type { ThinkingLevel } from "@earendil-works/pi-ai";import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";import { getAntigravityModelWireProfile } from "./wire.js";
export type AntigravityThinkingMode = "budget" | "google-level";
export interface AntigravityModelThinking { /** `thinkingLevel` (google-level) vs `thinkingBudget` (budget) wire field. */ mode: AntigravityThinkingMode; /** Per-effort token budgets for budget-mode models. Falls back to Google defaults. */ effortBudgets?: Partial<Record<ThinkingLevel, number>>; /** Emit an explicit thinking-off config; otherwise omit thinkingConfig entirely. */ suppressWhenOff: boolean; /** Reasoning is mandatory; "off" is hidden in the UI unless suppressWhenOff. */ requiresEffort: boolean; /** `-low` SKUs that also serve "minimal" must emit LOW (CCA rejects MINIMAL there). */ minimalLevelIsLow?: boolean;}
export interface AntigravityModelProfile { /** Claude vs Gemini/GPT-OSS wire paths. */ identityClass: "anthropic" | "gemini"; /** Upstream wire model id (equals the registered id for this extension). */ wireModelId: string; /** labels.model_enum token captured from the real antigravity/hub client. */ modelEnum?: string; /** Fixed generationConfig.maxOutputTokens enforced by the backend. */ maxOutputTokens?: number; thinking: AntigravityModelThinking;}
export function identityClassFor(modelId: string = ""): AntigravityModelProfile["identityClass"] { return (modelId || "").startsWith("claude-") ? "anthropic" : "gemini";}
interface AntigravityModelDef { id: string; name: string; reasoning: boolean; input: ("text" | "image")[]; contextWindow: number; maxTokens: number; cost: { input: number; output: number; cacheRead: number; cacheWrite: number }; thinking: AntigravityModelThinking;}
const THINK_BUDGET_DEFAULT: AntigravityModelThinking = { mode: "budget", suppressWhenOff: false, requiresEffort: false,};
const GEMINI_FLASH_BUDGETS: Partial<Record<ThinkingLevel, number>> = { minimal: 1000, low: 1000, medium: 4000, high: 10000,};
const GEMINI_PRO_BUDGETS: Partial<Record<ThinkingLevel, number>> = { low: 1001, high: 10001,};
const MODEL_DEFS: AntigravityModelDef[] = [ { id: "claude-opus-4-5-thinking", name: "Claude Opus 4.5", reasoning: true, input: ["text", "image"], contextWindow: 200000, maxTokens: 64000, cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 }, thinking: { ...THINK_BUDGET_DEFAULT }, }, { id: "claude-opus-4-6-thinking", name: "Claude Opus 4.6", reasoning: true, input: ["text", "image"], contextWindow: 250000, maxTokens: 64000, cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 }, thinking: { ...THINK_BUDGET_DEFAULT }, }, { id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5", reasoning: true, input: ["text", "image"], contextWindow: 1000000, maxTokens: 64000, cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, thinking: { ...THINK_BUDGET_DEFAULT }, }, { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", reasoning: true, input: ["text", "image"], contextWindow: 250000, maxTokens: 64000, cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, thinking: { ...THINK_BUDGET_DEFAULT }, }, { id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", reasoning: true, input: ["text", "image"], contextWindow: 1048576, maxTokens: 65535, cost: { input: 0.3, output: 2.5, cacheRead: 0.03, cacheWrite: 0 }, thinking: { ...THINK_BUDGET_DEFAULT }, }, { id: "gemini-2.5-flash-lite", name: "Gemini 2.5 Flash Lite", reasoning: true, input: ["text", "image"], contextWindow: 1048576, maxTokens: 65535, cost: { input: 0.1, output: 0.4, cacheRead: 0.01, cacheWrite: 0 }, thinking: { ...THINK_BUDGET_DEFAULT }, }, { id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", reasoning: true, input: ["text", "image"], contextWindow: 1048576, maxTokens: 65536, cost: { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0 }, thinking: { ...THINK_BUDGET_DEFAULT, requiresEffort: true }, }, { id: "gemini-3-pro-low", name: "Gemini 3 Pro (Low)", reasoning: true, input: ["text", "image"], contextWindow: 1048576, maxTokens: 65535, cost: { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 0 }, thinking: { mode: "google-level", suppressWhenOff: true, requiresEffort: true }, }, { id: "gemini-3.1-flash-lite", name: "Gemini 3.1 Flash Lite", reasoning: true, input: ["text", "image"], contextWindow: 1048576, maxTokens: 65535, cost: { input: 0.25, output: 1.5, cacheRead: 0.025, cacheWrite: 0 }, thinking: { mode: "google-level", suppressWhenOff: false, requiresEffort: true }, }, { id: "gemini-3.1-pro-low", name: "Gemini 3.1 Pro (Low)", reasoning: true, input: ["text", "image"], contextWindow: 1048576, maxTokens: 65535, cost: { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 0 }, thinking: { mode: "budget", effortBudgets: GEMINI_PRO_BUDGETS, suppressWhenOff: true, requiresEffort: true, }, }, { id: "gemini-pro-agent", name: "Gemini Pro Agent", reasoning: true, input: ["text", "image"], contextWindow: 1048576, maxTokens: 65535, cost: { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 0 }, thinking: { mode: "budget", effortBudgets: GEMINI_PRO_BUDGETS, suppressWhenOff: true, requiresEffort: true, }, }, { id: "gemini-3.5-flash-extra-low", name: "Gemini 3.5 Flash (Extra Low)", reasoning: true, input: ["text", "image"], contextWindow: 1048576, maxTokens: 65536, cost: { input: 1.5, output: 9, cacheRead: 0.15, cacheWrite: 0 }, thinking: { mode: "budget", effortBudgets: GEMINI_FLASH_BUDGETS, suppressWhenOff: true, requiresEffort: true, }, }, { id: "gemini-3.5-flash-low", name: "Gemini 3.5 Flash (Low)", reasoning: true, input: ["text", "image"], contextWindow: 1048576, maxTokens: 65536, cost: { input: 1.5, output: 9, cacheRead: 0.15, cacheWrite: 0 }, thinking: { mode: "budget", effortBudgets: GEMINI_FLASH_BUDGETS, suppressWhenOff: true, requiresEffort: true, }, }, { id: "gemini-3-flash-agent", name: "Gemini 3 Flash Agent", reasoning: true, input: ["text", "image"], contextWindow: 1048576, maxTokens: 65536, cost: { input: 0.5, output: 3, cacheRead: 0.05, cacheWrite: 0 }, thinking: { mode: "budget", effortBudgets: GEMINI_FLASH_BUDGETS, suppressWhenOff: true, requiresEffort: true, }, }, { id: "gemini-3.6-flash-low", name: "Gemini 3.6 Flash (Low)", reasoning: true, input: ["text", "image"], contextWindow: 1048576, maxTokens: 65536, cost: { input: 0.75, output: 3.75, cacheRead: 0.075, cacheWrite: 0 }, thinking: { mode: "google-level", suppressWhenOff: false, requiresEffort: true, minimalLevelIsLow: true, }, }, { id: "gemini-3.7-flash-low", name: "Gemini 3.7 Flash (Low)", reasoning: true, input: ["text", "image"], contextWindow: 1048576, maxTokens: 65536, cost: { input: 0.75, output: 3.75, cacheRead: 0.075, cacheWrite: 0 }, thinking: { mode: "google-level", suppressWhenOff: false, requiresEffort: true, minimalLevelIsLow: true, }, }, { id: "gpt-oss-120b-medium", name: "GPT OSS 120B", reasoning: true, input: ["text"], contextWindow: 131072, maxTokens: 32768, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, thinking: { ...THINK_BUDGET_DEFAULT }, },];
const MODEL_DEF_BY_ID = new Map(MODEL_DEFS.map((model) => [model.id, model]));
/** * Thinking-mode inference for live-discovered ids not in the static table, * mirroring oh-my-pi's per-family google-antigravity compat rules. */function inferAntigravityThinking(modelId: string = ""): AntigravityModelThinking { const lower = (modelId || "").toLowerCase(); if (lower.startsWith("claude-") || lower.startsWith("gpt-oss")) return { ...THINK_BUDGET_DEFAULT }; if (lower.startsWith("gemini-3-pro")) { return { mode: "google-level", suppressWhenOff: true, requiresEffort: true }; } if (/^gemini-3\.(6|7)-flash/.test(lower)) { return { mode: "google-level", suppressWhenOff: false, requiresEffort: true, minimalLevelIsLow: lower.endsWith("-low"), }; } if (lower === "gemini-3.1-flash-lite") { return { mode: "google-level", suppressWhenOff: false, requiresEffort: true }; } if (/^gemini-3(?:\.\d+)?-flash/.test(lower) || lower.includes("flash-agent")) { return { mode: "budget", effortBudgets: GEMINI_FLASH_BUDGETS, suppressWhenOff: true, requiresEffort: true }; } if (/^gemini-3(?:\.\d+)?-pro/.test(lower) || lower === "gemini-pro-agent") { return { mode: "budget", effortBudgets: GEMINI_PRO_BUDGETS, suppressWhenOff: true, requiresEffort: true }; } return { ...THINK_BUDGET_DEFAULT };}
export function getAntigravityProfile(modelId: string): AntigravityModelProfile { const def = MODEL_DEF_BY_ID.get(modelId); const wire = getAntigravityModelWireProfile(modelId); return { identityClass: def ? identityClassFor(def.id) : identityClassFor(modelId), wireModelId: modelId, modelEnum: wire?.modelEnum, maxOutputTokens: wire?.maxOutputTokens, thinking: def?.thinking ?? inferAntigravityThinking(modelId), };}
/** * Prime UI availability map. Levels omitted from the map default to supported * (except xhigh/max, which require an explicit mapping), and `null` hides a * level. Mirrors oh-my-pi's per-model supported efforts. */function thinkingLevelMap(def: AntigravityModelDef): ProviderModelConfig["thinkingLevelMap"] { const { thinking } = def; const levelMap: NonNullable<ProviderModelConfig["thinkingLevelMap"]> = { xhigh: null, max: null, };
if (thinking.requiresEffort && !thinking.suppressWhenOff) { levelMap.off = null; }
if (thinking.mode === "google-level") { levelMap.minimal = thinking.minimalLevelIsLow ? "LOW" : "MINIMAL"; levelMap.low = "LOW"; levelMap.medium = "MEDIUM"; levelMap.high = "HIGH"; }
return levelMap;}
export function toProviderModelConfig(def: AntigravityModelDef): ProviderModelConfig { return { id: def.id, name: def.name, reasoning: def.reasoning, input: def.input, cost: def.cost, contextWindow: def.contextWindow, maxTokens: def.maxTokens, thinkingLevelMap: thinkingLevelMap(def), };}
export const STATIC_PROVIDER_MODELS: ProviderModelConfig[] = MODEL_DEFS.map(toProviderModelConfig);12345678910{ "name": "antigravity-oauth", "version": "0.1.0", "description": "OAuth and provider extension for Google Antigravity (Cloud Code Assist)", "type": "module", "main": "index.ts", "scripts": { "test": "npx tsx test.ts" }}