From fe66edd943691f8eac295fef68ce36930c35fa05 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 30 Apr 2026 21:24:17 +0200 Subject: [PATCH] remove gemini cli and antigravity support --- packages/ai/CHANGELOG.md | 4 + packages/ai/README.md | 31 +- packages/ai/package.json | 4 - packages/ai/scripts/generate-models.ts | 214 ---- packages/ai/src/index.ts | 2 +- packages/ai/src/models.generated.ts | 276 ----- .../ai/src/providers/google-gemini-cli.ts | 996 ------------------ packages/ai/src/providers/google-shared.ts | 16 +- packages/ai/src/providers/google-vertex.ts | 2 +- packages/ai/src/providers/google.ts | 2 +- .../ai/src/providers/register-builtins.ts | 30 - packages/ai/src/types.ts | 3 - .../ai/src/utils/oauth/google-antigravity.ts | 455 -------- .../ai/src/utils/oauth/google-gemini-cli.ts | 597 ----------- packages/ai/src/utils/oauth/index.ts | 10 - packages/ai/test/abort.test.ts | 18 +- packages/ai/test/context-overflow.test.ts | 61 +- .../ai/test/cross-provider-handoff.test.ts | 3 - packages/ai/test/empty.test.ts | 152 +-- ...-gemini-cli-claude-thinking-header.test.ts | 103 -- .../google-gemini-cli-empty-stream.test.ts | 108 -- .../google-gemini-cli-retry-delay.test.ts | 53 - ...-shared-gemini3-unsigned-tool-call.test.ts | 10 +- ...e-shared-image-tool-result-routing.test.ts | 24 +- .../ai/test/google-thinking-disable.test.ts | 22 - .../google-tool-call-missing-args.test.ts | 105 -- packages/ai/test/image-tool-result.test.ts | 65 +- packages/ai/test/oauth.ts | 1 - packages/ai/test/responseid.test.ts | 28 +- packages/ai/test/stream.test.ts | 122 +-- packages/ai/test/tokens.test.ts | 52 +- .../ai/test/tool-call-without-result.test.ts | 44 +- packages/ai/test/total-tokens.test.ts | 78 +- packages/ai/test/unicode-surrogate.test.ts | 116 +- packages/coding-agent/README.md | 2 - packages/coding-agent/docs/custom-provider.md | 1 - packages/coding-agent/docs/providers.md | 4 - packages/coding-agent/docs/quickstart.md | 2 +- .../examples/extensions/README.md | 1 - .../extensions/antigravity-image-gen.ts | 418 -------- packages/coding-agent/src/cli/args.ts | 1 - .../coding-agent/src/core/model-resolver.ts | 2 - .../test/compaction-thinking-model.test.ts | 223 ---- packages/coding-agent/test/utilities.ts | 1 - 44 files changed, 41 insertions(+), 4421 deletions(-) delete mode 100644 packages/ai/src/providers/google-gemini-cli.ts delete mode 100644 packages/ai/src/utils/oauth/google-antigravity.ts delete mode 100644 packages/ai/src/utils/oauth/google-gemini-cli.ts delete mode 100644 packages/ai/test/google-gemini-cli-claude-thinking-header.test.ts delete mode 100644 packages/ai/test/google-gemini-cli-empty-stream.test.ts delete mode 100644 packages/ai/test/google-gemini-cli-retry-delay.test.ts delete mode 100644 packages/ai/test/google-tool-call-missing-args.test.ts delete mode 100644 packages/coding-agent/examples/extensions/antigravity-image-gen.ts delete mode 100644 packages/coding-agent/test/compaction-thinking-model.test.ts diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 9a3880a7..dbc15862 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -6,6 +6,10 @@ - Added `AssistantMessage.responseModel` on the openai-completions path: surfaces the concrete `chunk.model` when it differs from the requested id (e.g. OpenRouter `auto` -> `anthropic/...`). +### Removed + +- Removed built-in Google Gemini CLI and Google Antigravity provider, model, OAuth, and export support. + ### Fixed - Updated `@anthropic-ai/sdk` to `^0.91.1` to clear GHSA-p7fg-763f-g4gf audit findings ([#3992](https://github.com/badlogic/pi-mono/issues/3992)). diff --git a/packages/ai/README.md b/packages/ai/README.md index 18d3b945..e9ac59f3 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -63,8 +63,6 @@ Unified LLM API with automatic model discovery, provider configuration, token an - **Vercel AI Gateway** - **MiniMax** - **GitHub Copilot** (requires OAuth, see below) -- **Google Gemini CLI** (requires OAuth, see below) -- **Antigravity** (requires OAuth, see below) - **Amazon Bedrock** - **OpenCode Zen** - **OpenCode Go** @@ -630,7 +628,6 @@ The library uses a registry of API implementations. Built-in APIs include: - **`anthropic-messages`**: Anthropic Messages API (`streamAnthropic`, `AnthropicOptions`) - **`google-generative-ai`**: Google Generative AI API (`streamGoogle`, `GoogleOptions`) -- **`google-gemini-cli`**: Google Cloud Code Assist API (`streamGoogleGeminiCli`, `GoogleGeminiCliOptions`) - **`google-vertex`**: Google Vertex AI API (`streamGoogleVertex`, `GoogleVertexOptions`) - **`mistral-conversations`**: Mistral Conversations API (`streamMistral`, `MistralOptions`) - **`openai-completions`**: OpenAI Chat Completions API (`streamOpenAICompletions`, `OpenAICompletionsOptions`) @@ -1053,27 +1050,6 @@ const response = await complete(model, context, { }); ``` -#### Antigravity Version Override - -Set `PI_AI_ANTIGRAVITY_VERSION` to override the Antigravity User-Agent version when Google updates their requirements: - -```bash -export PI_AI_ANTIGRAVITY_VERSION="1.23.0" -``` - -#### Cache Retention - -Set `PI_CACHE_RETENTION=long` to extend prompt cache retention: - -| Provider | Default | With `PI_CACHE_RETENTION=long` | -|----------|---------|-------------------------------| -| Anthropic | 5 minutes | 1 hour | -| OpenAI | in-memory | 24 hours | - -This only affects direct API calls to `api.anthropic.com` and `api.openai.com`. Proxies and other providers are unaffected. - -> **Note**: Extended cache retention may increase costs for Anthropic (cache writes are charged at a higher rate). OpenAI's 24h retention has no additional cost. - ### Checking Environment Variables ```typescript @@ -1090,8 +1066,6 @@ Several providers require OAuth authentication instead of static API keys: - **Anthropic** (Claude Pro/Max subscription) - **OpenAI Codex** (ChatGPT Plus/Pro subscription, access to GPT-5.x Codex models) - **GitHub Copilot** (Copilot subscription) -- **Google Gemini CLI** (Gemini 2.0/2.5 via Google Cloud Code Assist; free tier or paid subscription) -- **Antigravity** (Free Gemini 3, Claude, GPT-OSS via Google Cloud) For paid Cloud Code Assist subscriptions, set `GOOGLE_CLOUD_PROJECT` or `GOOGLE_CLOUD_PROJECT_ID` to your project ID. @@ -1159,14 +1133,13 @@ import { loginOpenAICodex, loginGitHubCopilot, loginGeminiCli, - loginAntigravity, // Token management refreshOAuthToken, // (provider, credentials) => new credentials getOAuthApiKey, // (provider, credentialsMap) => { newCredentials, apiKey } | null // Types - type OAuthProvider, // 'anthropic' | 'openai-codex' | 'github-copilot' | 'google-gemini-cli' | 'google-antigravity' + type OAuthProvider, type OAuthCredentials, } from '@mariozechner/pi-ai/oauth'; ``` @@ -1228,8 +1201,6 @@ const response = await complete(model, { **GitHub Copilot**: If you get "The requested model is not supported" error, enable the model manually in VS Code: open Copilot Chat, click the model selector, select the model (warning icon), and click "Enable". -**Google Gemini CLI / Antigravity**: These use Google Cloud OAuth. The `apiKey` returned by `getOAuthApiKey()` is a JSON string containing both the token and project ID, which the library handles automatically. - ## Development ### Adding a New Provider diff --git a/packages/ai/package.json b/packages/ai/package.json index d82fff44..b31069bd 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -22,10 +22,6 @@ "types": "./dist/providers/google.d.ts", "import": "./dist/providers/google.js" }, - "./google-gemini-cli": { - "types": "./dist/providers/google-gemini-cli.d.ts", - "import": "./dist/providers/google-gemini-cli.js" - }, "./google-vertex": { "types": "./dist/providers/google-vertex.d.ts", "import": "./dist/providers/google-vertex.js" diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index f7eab41b..6f5e0fc8 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -823,12 +823,6 @@ async function generateModels() { ) { candidate.contextWindow = 1000000; } - if ( - candidate.provider === "google-antigravity" && - (candidate.id === "claude-opus-4-6-thinking" || candidate.id === "claude-sonnet-4-6") - ) { - candidate.contextWindow = 1000000; - } // OpenCode variants list Claude Sonnet 4/4.5 with 1M context, actual limit is 200K if ( @@ -1364,214 +1358,6 @@ async function generateModels() { }); } - // Google Cloud Code Assist models (Gemini CLI) - // Uses production endpoint, standard Gemini models only - const CLOUD_CODE_ASSIST_ENDPOINT = "https://cloudcode-pa.googleapis.com"; - const cloudCodeAssistModels: Model<"google-gemini-cli">[] = [ - { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro (Cloud Code Assist)", - api: "google-gemini-cli", - provider: "google-gemini-cli", - baseUrl: CLOUD_CODE_ASSIST_ENDPOINT, - reasoning: true, - input: ["text", "image"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 1048576, - maxTokens: 65535, - }, - { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash (Cloud Code Assist)", - api: "google-gemini-cli", - provider: "google-gemini-cli", - baseUrl: CLOUD_CODE_ASSIST_ENDPOINT, - reasoning: true, - input: ["text", "image"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 1048576, - maxTokens: 65535, - }, - { - id: "gemini-2.0-flash", - name: "Gemini 2.0 Flash (Cloud Code Assist)", - api: "google-gemini-cli", - provider: "google-gemini-cli", - baseUrl: CLOUD_CODE_ASSIST_ENDPOINT, - reasoning: false, - input: ["text", "image"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 1048576, - maxTokens: 8192, - }, - { - id: "gemini-3-pro-preview", - name: "Gemini 3 Pro Preview (Cloud Code Assist)", - api: "google-gemini-cli", - provider: "google-gemini-cli", - baseUrl: CLOUD_CODE_ASSIST_ENDPOINT, - reasoning: true, - input: ["text", "image"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 1048576, - maxTokens: 65535, - }, - { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash Preview (Cloud Code Assist)", - api: "google-gemini-cli", - provider: "google-gemini-cli", - baseUrl: CLOUD_CODE_ASSIST_ENDPOINT, - reasoning: true, - input: ["text", "image"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 1048576, - maxTokens: 65535, - }, - { - id: "gemini-3.1-flash-lite-preview", - name: "Gemini 3.1 Flash Lite Preview (Cloud Code Assist)", - api: "google-gemini-cli", - provider: "google-gemini-cli", - baseUrl: CLOUD_CODE_ASSIST_ENDPOINT, - reasoning: true, - input: ["text", "image"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 1048576, - maxTokens: 65535, - }, - { - id: "gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview (Cloud Code Assist)", - api: "google-gemini-cli", - provider: "google-gemini-cli", - baseUrl: CLOUD_CODE_ASSIST_ENDPOINT, - reasoning: true, - input: ["text", "image"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 1048576, - maxTokens: 65535, - }, - ]; - allModels.push(...cloudCodeAssistModels); - - // Antigravity models (Gemini 3, Claude, GPT-OSS via Google Cloud) - // Uses sandbox endpoint and different OAuth credentials for access to additional models - const ANTIGRAVITY_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com"; - const antigravityModels: Model<"google-gemini-cli">[] = [ - { - id: "gemini-3.1-pro-high", - name: "Gemini 3.1 Pro High (Antigravity)", - api: "google-gemini-cli", - provider: "google-antigravity", - baseUrl: ANTIGRAVITY_ENDPOINT, - reasoning: true, - input: ["text", "image"], - // the Model type doesn't seem to support having extended-context costs, so I'm just using the pricing for <200k input - cost: { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 2.375 }, - contextWindow: 1048576, - maxTokens: 65535, - }, - { - id: "gemini-3.1-pro-low", - name: "Gemini 3.1 Pro Low (Antigravity)", - api: "google-gemini-cli", - provider: "google-antigravity", - baseUrl: ANTIGRAVITY_ENDPOINT, - reasoning: true, - input: ["text", "image"], - // the Model type doesn't seem to support having extended-context costs, so I'm just using the pricing for <200k input - cost: { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 2.375 }, - contextWindow: 1048576, - maxTokens: 65535, - }, - { - id: "gemini-3-flash", - name: "Gemini 3 Flash (Antigravity)", - api: "google-gemini-cli", - provider: "google-antigravity", - baseUrl: ANTIGRAVITY_ENDPOINT, - reasoning: true, - input: ["text", "image"], - cost: { input: 0.5, output: 3, cacheRead: 0.5, cacheWrite: 0 }, - contextWindow: 1048576, - maxTokens: 65535, - }, - { - id: "claude-sonnet-4-5", - name: "Claude Sonnet 4.5 (Antigravity)", - api: "google-gemini-cli", - provider: "google-antigravity", - baseUrl: ANTIGRAVITY_ENDPOINT, - reasoning: false, - input: ["text", "image"], - cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, - contextWindow: 200000, - maxTokens: 64000, - }, - { - id: "claude-sonnet-4-5-thinking", - name: "Claude Sonnet 4.5 Thinking (Antigravity)", - api: "google-gemini-cli", - provider: "google-antigravity", - baseUrl: ANTIGRAVITY_ENDPOINT, - reasoning: true, - input: ["text", "image"], - cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, - contextWindow: 200000, - maxTokens: 64000, - }, - { - id: "claude-opus-4-5-thinking", - name: "Claude Opus 4.5 Thinking (Antigravity)", - api: "google-gemini-cli", - provider: "google-antigravity", - baseUrl: ANTIGRAVITY_ENDPOINT, - reasoning: true, - input: ["text", "image"], - cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 }, - contextWindow: 200000, - maxTokens: 64000, - }, - { - id: "claude-opus-4-6-thinking", - name: "Claude Opus 4.6 Thinking (Antigravity)", - api: "google-gemini-cli", - provider: "google-antigravity", - baseUrl: ANTIGRAVITY_ENDPOINT, - reasoning: true, - input: ["text", "image"], - cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 }, - contextWindow: 200000, - maxTokens: 128000, - }, - { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6 (Antigravity)", - api: "google-gemini-cli", - provider: "google-antigravity", - baseUrl: ANTIGRAVITY_ENDPOINT, - reasoning: true, - input: ["text", "image"], - cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, - contextWindow: 200000, - maxTokens: 64000, - }, - { - id: "gpt-oss-120b-medium", - name: "GPT-OSS 120B Medium (Antigravity)", - api: "google-gemini-cli", - provider: "google-antigravity", - baseUrl: ANTIGRAVITY_ENDPOINT, - reasoning: false, - input: ["text"], - cost: { input: 0.09, output: 0.36, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 131072, - maxTokens: 32768, - }, - ]; - allModels.push(...antigravityModels); - const VERTEX_BASE_URL = "https://{location}-aiplatform.googleapis.com"; const vertexModels: Model<"google-vertex">[] = [ { diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index 7235ecc7..b1e7f66e 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -9,7 +9,7 @@ export type { AnthropicEffort, AnthropicOptions, AnthropicThinkingDisplay } from export type { AzureOpenAIResponsesOptions } from "./providers/azure-openai-responses.js"; export * from "./providers/faux.js"; export type { GoogleOptions } from "./providers/google.js"; -export type { GoogleGeminiCliOptions, GoogleThinkingLevel } from "./providers/google-gemini-cli.js"; +export type { GoogleThinkingLevel } from "./providers/google-shared.js"; export type { GoogleVertexOptions } from "./providers/google-vertex.js"; export type { MistralOptions } from "./providers/mistral.js"; export type { OpenAICodexResponsesOptions } from "./providers/openai-codex-responses.js"; diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index c19e6117..9ada7041 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -4216,282 +4216,6 @@ export const MODELS = { maxTokens: 8192, } satisfies Model<"google-generative-ai">, }, - "google-antigravity": { - "claude-opus-4-5-thinking": { - id: "claude-opus-4-5-thinking", - name: "Claude Opus 4.5 Thinking (Antigravity)", - api: "google-gemini-cli", - provider: "google-antigravity", - baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"google-gemini-cli">, - "claude-opus-4-6-thinking": { - id: "claude-opus-4-6-thinking", - name: "Claude Opus 4.6 Thinking (Antigravity)", - api: "google-gemini-cli", - provider: "google-antigravity", - baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 128000, - } satisfies Model<"google-gemini-cli">, - "claude-sonnet-4-5": { - id: "claude-sonnet-4-5", - name: "Claude Sonnet 4.5 (Antigravity)", - api: "google-gemini-cli", - provider: "google-antigravity", - baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"google-gemini-cli">, - "claude-sonnet-4-5-thinking": { - id: "claude-sonnet-4-5-thinking", - name: "Claude Sonnet 4.5 Thinking (Antigravity)", - api: "google-gemini-cli", - provider: "google-antigravity", - baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"google-gemini-cli">, - "claude-sonnet-4-6": { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6 (Antigravity)", - api: "google-gemini-cli", - provider: "google-antigravity", - baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"google-gemini-cli">, - "gemini-3-flash": { - id: "gemini-3-flash", - name: "Gemini 3 Flash (Antigravity)", - api: "google-gemini-cli", - provider: "google-antigravity", - baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.5, - output: 3, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65535, - } satisfies Model<"google-gemini-cli">, - "gemini-3.1-pro-high": { - id: "gemini-3.1-pro-high", - name: "Gemini 3.1 Pro High (Antigravity)", - api: "google-gemini-cli", - provider: "google-antigravity", - baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.2, - cacheWrite: 2.375, - }, - contextWindow: 1048576, - maxTokens: 65535, - } satisfies Model<"google-gemini-cli">, - "gemini-3.1-pro-low": { - id: "gemini-3.1-pro-low", - name: "Gemini 3.1 Pro Low (Antigravity)", - api: "google-gemini-cli", - provider: "google-antigravity", - baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.2, - cacheWrite: 2.375, - }, - contextWindow: 1048576, - maxTokens: 65535, - } satisfies Model<"google-gemini-cli">, - "gpt-oss-120b-medium": { - id: "gpt-oss-120b-medium", - name: "GPT-OSS 120B Medium (Antigravity)", - api: "google-gemini-cli", - provider: "google-antigravity", - baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.09, - output: 0.36, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"google-gemini-cli">, - }, - "google-gemini-cli": { - "gemini-2.0-flash": { - id: "gemini-2.0-flash", - name: "Gemini 2.0 Flash (Cloud Code Assist)", - api: "google-gemini-cli", - provider: "google-gemini-cli", - baseUrl: "https://cloudcode-pa.googleapis.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 8192, - } satisfies Model<"google-gemini-cli">, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash (Cloud Code Assist)", - api: "google-gemini-cli", - provider: "google-gemini-cli", - baseUrl: "https://cloudcode-pa.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65535, - } satisfies Model<"google-gemini-cli">, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro (Cloud Code Assist)", - api: "google-gemini-cli", - provider: "google-gemini-cli", - baseUrl: "https://cloudcode-pa.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65535, - } satisfies Model<"google-gemini-cli">, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash Preview (Cloud Code Assist)", - api: "google-gemini-cli", - provider: "google-gemini-cli", - baseUrl: "https://cloudcode-pa.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65535, - } satisfies Model<"google-gemini-cli">, - "gemini-3-pro-preview": { - id: "gemini-3-pro-preview", - name: "Gemini 3 Pro Preview (Cloud Code Assist)", - api: "google-gemini-cli", - provider: "google-gemini-cli", - baseUrl: "https://cloudcode-pa.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65535, - } satisfies Model<"google-gemini-cli">, - "gemini-3.1-flash-lite-preview": { - id: "gemini-3.1-flash-lite-preview", - name: "Gemini 3.1 Flash Lite Preview (Cloud Code Assist)", - api: "google-gemini-cli", - provider: "google-gemini-cli", - baseUrl: "https://cloudcode-pa.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65535, - } satisfies Model<"google-gemini-cli">, - "gemini-3.1-pro-preview": { - id: "gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview (Cloud Code Assist)", - api: "google-gemini-cli", - provider: "google-gemini-cli", - baseUrl: "https://cloudcode-pa.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65535, - } satisfies Model<"google-gemini-cli">, - }, "google-vertex": { "gemini-1.5-flash": { id: "gemini-1.5-flash", diff --git a/packages/ai/src/providers/google-gemini-cli.ts b/packages/ai/src/providers/google-gemini-cli.ts deleted file mode 100644 index daaa711b..00000000 --- a/packages/ai/src/providers/google-gemini-cli.ts +++ /dev/null @@ -1,996 +0,0 @@ -/** - * Google Gemini CLI / Antigravity provider. - * Shared implementation for both google-gemini-cli and google-antigravity providers. - * Uses the Cloud Code Assist API endpoint to access Gemini and Claude models. - */ - -import type { Content, ThinkingConfig } from "@google/genai"; -import { calculateCost } from "../models.js"; -import type { - Api, - AssistantMessage, - Context, - Model, - SimpleStreamOptions, - StreamFunction, - StreamOptions, - TextContent, - ThinkingBudgets, - ThinkingContent, - ThinkingLevel, - ToolCall, -} from "../types.js"; -import { AssistantMessageEventStream } from "../utils/event-stream.js"; -import { headersToRecord } from "../utils/headers.js"; -import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; -import { - convertMessages, - convertTools, - isThinkingPart, - mapStopReasonString, - mapToolChoice, - retainThoughtSignature, -} from "./google-shared.js"; -import { buildBaseOptions, clampReasoning } from "./simple-options.js"; - -/** - * Thinking level for Gemini 3 models. - * Mirrors Google's ThinkingLevel enum values. - */ -export type GoogleThinkingLevel = "THINKING_LEVEL_UNSPECIFIED" | "MINIMAL" | "LOW" | "MEDIUM" | "HIGH"; - -export interface GoogleGeminiCliOptions extends StreamOptions { - toolChoice?: "auto" | "none" | "any"; - /** - * Thinking/reasoning configuration. - * - Gemini 2.x models: use `budgetTokens` to set the thinking budget - * - Gemini 3 models (gemini-3-pro-*, gemini-3-flash-*): use `level` instead - * - * When using `streamSimple`, this is handled automatically based on the model. - */ - thinking?: { - enabled: boolean; - /** Thinking budget in tokens. Use for Gemini 2.x models. */ - budgetTokens?: number; - /** Thinking level. Use for Gemini 3 models (LOW/HIGH for Pro, MINIMAL/LOW/MEDIUM/HIGH for Flash). */ - level?: GoogleThinkingLevel; - }; - projectId?: string; -} - -const DEFAULT_ENDPOINT = "https://cloudcode-pa.googleapis.com"; -const ANTIGRAVITY_DAILY_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com"; -const ANTIGRAVITY_AUTOPUSH_ENDPOINT = "https://autopush-cloudcode-pa.sandbox.googleapis.com"; -const ANTIGRAVITY_ENDPOINT_FALLBACKS = [ - ANTIGRAVITY_DAILY_ENDPOINT, - ANTIGRAVITY_AUTOPUSH_ENDPOINT, - DEFAULT_ENDPOINT, -] as const; -// Headers for Gemini CLI (prod endpoint) -const GEMINI_CLI_HEADERS = { - "User-Agent": "google-cloud-sdk vscode_cloudshelleditor/0.1", - "X-Goog-Api-Client": "gl-node/22.17.0", - "Client-Metadata": JSON.stringify({ - ideType: "IDE_UNSPECIFIED", - platform: "PLATFORM_UNSPECIFIED", - pluginType: "GEMINI", - }), -}; - -// Headers for Antigravity (sandbox endpoint) - requires specific User-Agent -const DEFAULT_ANTIGRAVITY_VERSION = "1.107.0"; - -function getAntigravityHeaders() { - const version = process.env.PI_AI_ANTIGRAVITY_VERSION || DEFAULT_ANTIGRAVITY_VERSION; - return { - "User-Agent": `antigravity/${version} darwin/arm64`, - }; -} - -// Antigravity system instruction (compact version from CLIProxyAPI). -const ANTIGRAVITY_SYSTEM_INSTRUCTION = - "You are Antigravity, a powerful agentic AI coding assistant designed by the Google Deepmind team working on Advanced Agentic Coding." + - "You are pair programming with a USER to solve their coding task. The task may require creating a new codebase, modifying or debugging an existing codebase, or simply answering a question." + - "**Absolute paths only**" + - "**Proactiveness**"; - -// Counter for generating unique tool call IDs -let toolCallCounter = 0; - -// Retry configuration -const MAX_RETRIES = 3; -const BASE_DELAY_MS = 1000; -const MAX_EMPTY_STREAM_RETRIES = 2; -const EMPTY_STREAM_BASE_DELAY_MS = 500; -const CLAUDE_THINKING_BETA_HEADER = "interleaved-thinking-2025-05-14"; - -/** - * Extract retry delay from Gemini error response (in milliseconds). - * Checks headers first (Retry-After, x-ratelimit-reset, x-ratelimit-reset-after), - * then parses body patterns like: - * - "Your quota will reset after 39s" - * - "Your quota will reset after 18h31m10s" - * - "Please retry in Xs" or "Please retry in Xms" - * - "retryDelay": "34.074824224s" (JSON field) - */ -export function extractRetryDelay(errorText: string, response?: Response | Headers): number | undefined { - const normalizeDelay = (ms: number): number | undefined => (ms > 0 ? Math.ceil(ms + 1000) : undefined); - - const headers = response instanceof Headers ? response : response?.headers; - if (headers) { - const retryAfter = headers.get("retry-after"); - if (retryAfter) { - const retryAfterSeconds = Number(retryAfter); - if (Number.isFinite(retryAfterSeconds)) { - const delay = normalizeDelay(retryAfterSeconds * 1000); - if (delay !== undefined) { - return delay; - } - } - const retryAfterDate = new Date(retryAfter); - const retryAfterMs = retryAfterDate.getTime(); - if (!Number.isNaN(retryAfterMs)) { - const delay = normalizeDelay(retryAfterMs - Date.now()); - if (delay !== undefined) { - return delay; - } - } - } - - const rateLimitReset = headers.get("x-ratelimit-reset"); - if (rateLimitReset) { - const resetSeconds = Number.parseInt(rateLimitReset, 10); - if (!Number.isNaN(resetSeconds)) { - const delay = normalizeDelay(resetSeconds * 1000 - Date.now()); - if (delay !== undefined) { - return delay; - } - } - } - - const rateLimitResetAfter = headers.get("x-ratelimit-reset-after"); - if (rateLimitResetAfter) { - const resetAfterSeconds = Number(rateLimitResetAfter); - if (Number.isFinite(resetAfterSeconds)) { - const delay = normalizeDelay(resetAfterSeconds * 1000); - if (delay !== undefined) { - return delay; - } - } - } - } - - // Pattern 1: "Your quota will reset after ..." (formats: "18h31m10s", "10m15s", "6s", "39s") - const durationMatch = errorText.match(/reset after (?:(\d+)h)?(?:(\d+)m)?(\d+(?:\.\d+)?)s/i); - if (durationMatch) { - const hours = durationMatch[1] ? parseInt(durationMatch[1], 10) : 0; - const minutes = durationMatch[2] ? parseInt(durationMatch[2], 10) : 0; - const seconds = parseFloat(durationMatch[3]); - if (!Number.isNaN(seconds)) { - const totalMs = ((hours * 60 + minutes) * 60 + seconds) * 1000; - const delay = normalizeDelay(totalMs); - if (delay !== undefined) { - return delay; - } - } - } - - // Pattern 2: "Please retry in X[ms|s]" - const retryInMatch = errorText.match(/Please retry in ([0-9.]+)(ms|s)/i); - if (retryInMatch?.[1]) { - const value = parseFloat(retryInMatch[1]); - if (!Number.isNaN(value) && value > 0) { - const ms = retryInMatch[2].toLowerCase() === "ms" ? value : value * 1000; - const delay = normalizeDelay(ms); - if (delay !== undefined) { - return delay; - } - } - } - - // Pattern 3: "retryDelay": "34.074824224s" (JSON field in error details) - const retryDelayMatch = errorText.match(/"retryDelay":\s*"([0-9.]+)(ms|s)"/i); - if (retryDelayMatch?.[1]) { - const value = parseFloat(retryDelayMatch[1]); - if (!Number.isNaN(value) && value > 0) { - const ms = retryDelayMatch[2].toLowerCase() === "ms" ? value : value * 1000; - const delay = normalizeDelay(ms); - if (delay !== undefined) { - return delay; - } - } - } - - return undefined; -} - -function needsClaudeThinkingBetaHeader(model: Model<"google-gemini-cli">): boolean { - return model.provider === "google-antigravity" && model.id.startsWith("claude-") && model.reasoning; -} - -function isGemini3ProModel(modelId: string): boolean { - return /gemini-3(?:\.1)?-pro/.test(modelId.toLowerCase()); -} - -function isGemini3FlashModel(modelId: string): boolean { - return /gemini-3(?:\.1)?-flash/.test(modelId.toLowerCase()); -} - -function isGemini3Model(modelId: string): boolean { - return isGemini3ProModel(modelId) || isGemini3FlashModel(modelId); -} - -/** - * Check if an error is retryable (rate limit, server error, network error, etc.) - */ -function isRetryableError(status: number, errorText: string): boolean { - if (status === 429 || status === 500 || status === 502 || status === 503 || status === 504) { - return true; - } - return /resource.?exhausted|rate.?limit|overloaded|service.?unavailable|other.?side.?closed/i.test(errorText); -} - -/** - * Extract a clean, user-friendly error message from Google API error response. - * Parses JSON error responses and returns just the message field. - */ -function extractErrorMessage(errorText: string): string { - try { - const parsed = JSON.parse(errorText) as { error?: { message?: string } }; - if (parsed.error?.message) { - return parsed.error.message; - } - } catch { - // Not JSON, return as-is - } - return errorText; -} - -/** - * Sleep for a given number of milliseconds, respecting abort signal. - */ -function sleep(ms: number, signal?: AbortSignal): Promise { - return new Promise((resolve, reject) => { - if (signal?.aborted) { - reject(new Error("Request was aborted")); - return; - } - const timeout = setTimeout(resolve, ms); - signal?.addEventListener("abort", () => { - clearTimeout(timeout); - reject(new Error("Request was aborted")); - }); - }); -} - -interface CloudCodeAssistRequest { - project: string; - model: string; - request: { - contents: Content[]; - sessionId?: string; - systemInstruction?: { role?: string; parts: { text: string }[] }; - generationConfig?: { - maxOutputTokens?: number; - temperature?: number; - thinkingConfig?: ThinkingConfig; - }; - tools?: ReturnType; - toolConfig?: { - functionCallingConfig: { - mode: ReturnType; - }; - }; - }; - requestType?: string; - userAgent?: string; - requestId?: string; -} - -interface CloudCodeAssistResponseChunk { - response?: { - candidates?: Array<{ - content?: { - role: string; - parts?: Array<{ - text?: string; - thought?: boolean; - thoughtSignature?: string; - functionCall?: { - name: string; - args: Record; - id?: string; - }; - }>; - }; - finishReason?: string; - }>; - usageMetadata?: { - promptTokenCount?: number; - candidatesTokenCount?: number; - thoughtsTokenCount?: number; - totalTokenCount?: number; - cachedContentTokenCount?: number; - }; - modelVersion?: string; - responseId?: string; - }; - traceId?: string; -} - -export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli", GoogleGeminiCliOptions> = ( - model: Model<"google-gemini-cli">, - context: Context, - options?: GoogleGeminiCliOptions, -): AssistantMessageEventStream => { - const stream = new AssistantMessageEventStream(); - - (async () => { - const output: AssistantMessage = { - role: "assistant", - content: [], - api: "google-gemini-cli" as Api, - provider: model.provider, - model: model.id, - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - }, - stopReason: "stop", - timestamp: Date.now(), - }; - - try { - // apiKey is JSON-encoded: { token, projectId } - const apiKeyRaw = options?.apiKey; - if (!apiKeyRaw) { - throw new Error("Google Cloud Code Assist requires OAuth authentication. Use /login to authenticate."); - } - - let accessToken: string; - let projectId: string; - - try { - const parsed = JSON.parse(apiKeyRaw) as { token: string; projectId: string }; - accessToken = parsed.token; - projectId = parsed.projectId; - } catch { - throw new Error("Invalid Google Cloud Code Assist credentials. Use /login to re-authenticate."); - } - - if (!accessToken || !projectId) { - throw new Error("Missing token or projectId in Google Cloud credentials. Use /login to re-authenticate."); - } - - const isAntigravity = model.provider === "google-antigravity"; - const baseUrl = model.baseUrl?.trim(); - const endpoints = baseUrl ? [baseUrl] : isAntigravity ? ANTIGRAVITY_ENDPOINT_FALLBACKS : [DEFAULT_ENDPOINT]; - - let requestBody = buildRequest(model, context, projectId, options, isAntigravity); - const nextRequestBody = await options?.onPayload?.(requestBody, model); - if (nextRequestBody !== undefined) { - requestBody = nextRequestBody as CloudCodeAssistRequest; - } - const headers = isAntigravity ? getAntigravityHeaders() : GEMINI_CLI_HEADERS; - - const requestHeaders = { - Authorization: `Bearer ${accessToken}`, - "Content-Type": "application/json", - Accept: "text/event-stream", - ...headers, - ...(needsClaudeThinkingBetaHeader(model) ? { "anthropic-beta": CLAUDE_THINKING_BETA_HEADER } : {}), - ...options?.headers, - }; - const requestBodyJson = JSON.stringify(requestBody); - - // Fetch with retry logic for rate limits, transient errors, and endpoint fallbacks. - // On 403/404, immediately try the next endpoint (no delay). - // On 429/5xx, retry with backoff on the same or next endpoint. - let response: Response | undefined; - let lastError: Error | undefined; - let requestUrl: string | undefined; - let endpointIndex = 0; - - for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { - if (options?.signal?.aborted) { - throw new Error("Request was aborted"); - } - - try { - const endpoint = endpoints[endpointIndex]; - requestUrl = `${endpoint}/v1internal:streamGenerateContent?alt=sse`; - response = await fetch(requestUrl, { - method: "POST", - headers: requestHeaders, - body: requestBodyJson, - signal: options?.signal, - }); - await options?.onResponse?.( - { status: response.status, headers: headersToRecord(response.headers) }, - model, - ); - - if (response.ok) { - break; // Success, exit retry loop - } - - const errorText = await response.text(); - - // On 403/404, cascade to the next endpoint immediately (no delay) - if ((response.status === 403 || response.status === 404) && endpointIndex < endpoints.length - 1) { - endpointIndex++; - continue; - } - - // Check if retryable (429, 5xx, network patterns) - if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) { - // Advance endpoint if possible - if (endpointIndex < endpoints.length - 1) { - endpointIndex++; - } - - // Use server-provided delay or exponential backoff - const serverDelay = extractRetryDelay(errorText, response); - const delayMs = serverDelay ?? BASE_DELAY_MS * 2 ** attempt; - - // Check if server delay exceeds max allowed (default: 60s) - const maxDelayMs = options?.maxRetryDelayMs ?? 60000; - if (maxDelayMs > 0 && serverDelay && serverDelay > maxDelayMs) { - const delaySeconds = Math.ceil(serverDelay / 1000); - throw new Error( - `Server requested ${delaySeconds}s retry delay (max: ${Math.ceil(maxDelayMs / 1000)}s). ${extractErrorMessage(errorText)}`, - ); - } - - await sleep(delayMs, options?.signal); - continue; - } - - // Not retryable or max retries exceeded - throw new Error(`Cloud Code Assist API error (${response.status}): ${extractErrorMessage(errorText)}`); - } catch (error) { - // Check for abort - fetch throws AbortError, our code throws "Request was aborted" - if (error instanceof Error) { - if (error.name === "AbortError" || error.message === "Request was aborted") { - throw new Error("Request was aborted"); - } - } - // Extract detailed error message from fetch errors (Node includes cause) - lastError = error instanceof Error ? error : new Error(String(error)); - if (lastError.message === "fetch failed" && lastError.cause instanceof Error) { - lastError = new Error(`Network error: ${lastError.cause.message}`); - } - // Network errors are retryable - if (attempt < MAX_RETRIES) { - const delayMs = BASE_DELAY_MS * 2 ** attempt; - await sleep(delayMs, options?.signal); - continue; - } - throw lastError; - } - } - - if (!response || !response.ok) { - throw lastError ?? new Error("Failed to get response after retries"); - } - - let started = false; - const ensureStarted = () => { - if (!started) { - stream.push({ type: "start", partial: output }); - started = true; - } - }; - - const resetOutput = () => { - output.content = []; - output.usage = { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - }; - output.stopReason = "stop"; - output.errorMessage = undefined; - output.timestamp = Date.now(); - started = false; - }; - - const streamResponse = async (activeResponse: Response): Promise => { - if (!activeResponse.body) { - throw new Error("No response body"); - } - - let hasContent = false; - let currentBlock: TextContent | ThinkingContent | null = null; - const blocks = output.content; - const blockIndex = () => blocks.length - 1; - - // Read SSE stream - const reader = activeResponse.body.getReader(); - const decoder = new TextDecoder(); - let buffer = ""; - - // Set up abort handler to cancel reader when signal fires - const abortHandler = () => { - void reader.cancel().catch(() => {}); - }; - options?.signal?.addEventListener("abort", abortHandler); - - try { - while (true) { - // Check abort signal before each read - if (options?.signal?.aborted) { - throw new Error("Request was aborted"); - } - - const { done, value } = await reader.read(); - if (done) break; - - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split("\n"); - buffer = lines.pop() || ""; - - for (const line of lines) { - if (!line.startsWith("data:")) continue; - - const jsonStr = line.slice(5).trim(); - if (!jsonStr) continue; - - let chunk: CloudCodeAssistResponseChunk; - try { - chunk = JSON.parse(jsonStr); - } catch { - continue; - } - - // Unwrap the response - const responseData = chunk.response; - if (!responseData) continue; - // Cloud Code Assist mirrors Gemini's responseId field. Keep the first non-empty one. - // A single streamed response should retain the same ID across chunks. - output.responseId ||= responseData.responseId; - - const candidate = responseData.candidates?.[0]; - if (candidate?.content?.parts) { - for (const part of candidate.content.parts) { - if (part.text !== undefined) { - hasContent = true; - const isThinking = isThinkingPart(part); - if ( - !currentBlock || - (isThinking && currentBlock.type !== "thinking") || - (!isThinking && currentBlock.type !== "text") - ) { - if (currentBlock) { - if (currentBlock.type === "text") { - stream.push({ - type: "text_end", - contentIndex: blocks.length - 1, - content: currentBlock.text, - partial: output, - }); - } else { - stream.push({ - type: "thinking_end", - contentIndex: blockIndex(), - content: currentBlock.thinking, - partial: output, - }); - } - } - if (isThinking) { - currentBlock = { type: "thinking", thinking: "", thinkingSignature: undefined }; - output.content.push(currentBlock); - ensureStarted(); - stream.push({ - type: "thinking_start", - contentIndex: blockIndex(), - partial: output, - }); - } else { - currentBlock = { type: "text", text: "" }; - output.content.push(currentBlock); - ensureStarted(); - stream.push({ type: "text_start", contentIndex: blockIndex(), partial: output }); - } - } - if (currentBlock.type === "thinking") { - currentBlock.thinking += part.text; - currentBlock.thinkingSignature = retainThoughtSignature( - currentBlock.thinkingSignature, - part.thoughtSignature, - ); - stream.push({ - type: "thinking_delta", - contentIndex: blockIndex(), - delta: part.text, - partial: output, - }); - } else { - currentBlock.text += part.text; - currentBlock.textSignature = retainThoughtSignature( - currentBlock.textSignature, - part.thoughtSignature, - ); - stream.push({ - type: "text_delta", - contentIndex: blockIndex(), - delta: part.text, - partial: output, - }); - } - } - - if (part.functionCall) { - hasContent = true; - if (currentBlock) { - if (currentBlock.type === "text") { - stream.push({ - type: "text_end", - contentIndex: blockIndex(), - content: currentBlock.text, - partial: output, - }); - } else { - stream.push({ - type: "thinking_end", - contentIndex: blockIndex(), - content: currentBlock.thinking, - partial: output, - }); - } - currentBlock = null; - } - - const providedId = part.functionCall.id; - const needsNewId = - !providedId || - output.content.some((b) => b.type === "toolCall" && b.id === providedId); - const toolCallId = needsNewId - ? `${part.functionCall.name}_${Date.now()}_${++toolCallCounter}` - : providedId; - - const toolCall: ToolCall = { - type: "toolCall", - id: toolCallId, - name: part.functionCall.name || "", - arguments: (part.functionCall.args as Record) ?? {}, - ...(part.thoughtSignature && { thoughtSignature: part.thoughtSignature }), - }; - - output.content.push(toolCall); - ensureStarted(); - stream.push({ type: "toolcall_start", contentIndex: blockIndex(), partial: output }); - stream.push({ - type: "toolcall_delta", - contentIndex: blockIndex(), - delta: JSON.stringify(toolCall.arguments), - partial: output, - }); - stream.push({ - type: "toolcall_end", - contentIndex: blockIndex(), - toolCall, - partial: output, - }); - } - } - } - - if (candidate?.finishReason) { - output.stopReason = mapStopReasonString(candidate.finishReason); - if (output.content.some((b) => b.type === "toolCall")) { - output.stopReason = "toolUse"; - } - } - - if (responseData.usageMetadata) { - // promptTokenCount includes cachedContentTokenCount, so subtract to get fresh input - const promptTokens = responseData.usageMetadata.promptTokenCount || 0; - const cacheReadTokens = responseData.usageMetadata.cachedContentTokenCount || 0; - output.usage = { - input: promptTokens - cacheReadTokens, - output: - (responseData.usageMetadata.candidatesTokenCount || 0) + - (responseData.usageMetadata.thoughtsTokenCount || 0), - cacheRead: cacheReadTokens, - cacheWrite: 0, - totalTokens: responseData.usageMetadata.totalTokenCount || 0, - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - total: 0, - }, - }; - calculateCost(model, output.usage); - } - } - } - } finally { - options?.signal?.removeEventListener("abort", abortHandler); - } - - if (currentBlock) { - if (currentBlock.type === "text") { - stream.push({ - type: "text_end", - contentIndex: blockIndex(), - content: currentBlock.text, - partial: output, - }); - } else { - stream.push({ - type: "thinking_end", - contentIndex: blockIndex(), - content: currentBlock.thinking, - partial: output, - }); - } - } - - return hasContent; - }; - - let receivedContent = false; - let currentResponse = response; - - for (let emptyAttempt = 0; emptyAttempt <= MAX_EMPTY_STREAM_RETRIES; emptyAttempt++) { - if (options?.signal?.aborted) { - throw new Error("Request was aborted"); - } - - if (emptyAttempt > 0) { - const backoffMs = EMPTY_STREAM_BASE_DELAY_MS * 2 ** (emptyAttempt - 1); - await sleep(backoffMs, options?.signal); - - if (!requestUrl) { - throw new Error("Missing request URL"); - } - - currentResponse = await fetch(requestUrl, { - method: "POST", - headers: requestHeaders, - body: requestBodyJson, - signal: options?.signal, - }); - await options?.onResponse?.( - { status: currentResponse.status, headers: headersToRecord(currentResponse.headers) }, - model, - ); - - if (!currentResponse.ok) { - const retryErrorText = await currentResponse.text(); - throw new Error(`Cloud Code Assist API error (${currentResponse.status}): ${retryErrorText}`); - } - } - - const streamed = await streamResponse(currentResponse); - if (streamed) { - receivedContent = true; - break; - } - - if (emptyAttempt < MAX_EMPTY_STREAM_RETRIES) { - resetOutput(); - } - } - - if (!receivedContent) { - throw new Error("Cloud Code Assist API returned an empty response"); - } - - if (options?.signal?.aborted) { - throw new Error("Request was aborted"); - } - - if (output.stopReason === "aborted" || output.stopReason === "error") { - throw new Error("An unknown error occurred"); - } - - stream.push({ type: "done", reason: output.stopReason, message: output }); - stream.end(); - } catch (error) { - for (const block of output.content) { - if ("index" in block) { - delete (block as { index?: number }).index; - } - } - output.stopReason = options?.signal?.aborted ? "aborted" : "error"; - output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error); - stream.push({ type: "error", reason: output.stopReason, error: output }); - stream.end(); - } - })(); - - return stream; -}; - -export const streamSimpleGoogleGeminiCli: StreamFunction<"google-gemini-cli", SimpleStreamOptions> = ( - model: Model<"google-gemini-cli">, - context: Context, - options?: SimpleStreamOptions, -): AssistantMessageEventStream => { - const apiKey = options?.apiKey; - if (!apiKey) { - throw new Error("Google Cloud Code Assist requires OAuth authentication. Use /login to authenticate."); - } - - const base = buildBaseOptions(model, options, apiKey); - if (!options?.reasoning) { - return streamGoogleGeminiCli(model, context, { - ...base, - thinking: { enabled: false }, - } satisfies GoogleGeminiCliOptions); - } - - const effort = clampReasoning(options.reasoning)!; - if (isGemini3Model(model.id)) { - return streamGoogleGeminiCli(model, context, { - ...base, - thinking: { - enabled: true, - level: getGeminiCliThinkingLevel(effort, model.id), - }, - } satisfies GoogleGeminiCliOptions); - } - - const defaultBudgets: ThinkingBudgets = { - minimal: 1024, - low: 2048, - medium: 8192, - high: 16384, - }; - const budgets = { ...defaultBudgets, ...options.thinkingBudgets }; - - const minOutputTokens = 1024; - let thinkingBudget = budgets[effort]!; - const maxTokens = Math.min((base.maxTokens || 0) + thinkingBudget, model.maxTokens); - - if (maxTokens <= thinkingBudget) { - thinkingBudget = Math.max(0, maxTokens - minOutputTokens); - } - - return streamGoogleGeminiCli(model, context, { - ...base, - maxTokens, - thinking: { - enabled: true, - budgetTokens: thinkingBudget, - }, - } satisfies GoogleGeminiCliOptions); -}; - -export function buildRequest( - model: Model<"google-gemini-cli">, - context: Context, - projectId: string, - options: GoogleGeminiCliOptions = {}, - isAntigravity = false, -): CloudCodeAssistRequest { - const contents = convertMessages(model, context); - - const generationConfig: CloudCodeAssistRequest["request"]["generationConfig"] = {}; - if (options.temperature !== undefined) { - generationConfig.temperature = options.temperature; - } - if (options.maxTokens !== undefined) { - generationConfig.maxOutputTokens = options.maxTokens; - } - - // Thinking config - if (options.thinking?.enabled && model.reasoning) { - generationConfig.thinkingConfig = { - includeThoughts: true, - }; - // Gemini 3 models use thinkingLevel, older models use thinkingBudget - if (options.thinking.level !== undefined) { - // Cast to any since our GoogleThinkingLevel mirrors Google's ThinkingLevel enum values - generationConfig.thinkingConfig.thinkingLevel = options.thinking.level as any; - } else if (options.thinking.budgetTokens !== undefined) { - generationConfig.thinkingConfig.thinkingBudget = options.thinking.budgetTokens; - } - } else if (model.reasoning && options.thinking && !options.thinking.enabled) { - generationConfig.thinkingConfig = getDisabledThinkingConfig(model.id); - } - - const request: CloudCodeAssistRequest["request"] = { - contents, - }; - - request.sessionId = options.sessionId; - - // System instruction must be object with parts, not plain string - if (context.systemPrompt) { - request.systemInstruction = { - parts: [{ text: sanitizeSurrogates(context.systemPrompt) }], - }; - } - - if (Object.keys(generationConfig).length > 0) { - request.generationConfig = generationConfig; - } - - if (context.tools && context.tools.length > 0) { - // Claude models on Cloud Code Assist need the legacy `parameters` field; - // the API translates it into Anthropic's `input_schema`. - const useParameters = model.id.startsWith("claude-"); - request.tools = convertTools(context.tools, useParameters); - if (options.toolChoice) { - request.toolConfig = { - functionCallingConfig: { - mode: mapToolChoice(options.toolChoice), - }, - }; - } - } - - if (isAntigravity) { - const existingParts = request.systemInstruction?.parts ?? []; - request.systemInstruction = { - role: "user", - parts: [ - { text: ANTIGRAVITY_SYSTEM_INSTRUCTION }, - { text: `Please ignore following [ignore]${ANTIGRAVITY_SYSTEM_INSTRUCTION}[/ignore]` }, - ...existingParts, - ], - }; - } - - return { - project: projectId, - model: model.id, - request, - ...(isAntigravity ? { requestType: "agent" } : {}), - userAgent: isAntigravity ? "antigravity" : "pi-coding-agent", - requestId: `${isAntigravity ? "agent" : "pi"}-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`, - }; -} - -type ClampedThinkingLevel = Exclude; - -function getDisabledThinkingConfig(modelId: string): ThinkingConfig { - // Google docs: Gemini 3.1 Pro cannot disable thinking, and Gemini 3 Flash / Flash-Lite - // do not support full thinking-off either. For Gemini 3 models, use the lowest supported - // thinkingLevel without includeThoughts so hidden thinking remains invisible to pi. - if (isGemini3ProModel(modelId)) { - return { thinkingLevel: "LOW" as any }; - } - if (isGemini3FlashModel(modelId)) { - return { thinkingLevel: "MINIMAL" as any }; - } - - // Gemini 2.x supports disabling via thinkingBudget = 0. - return { thinkingBudget: 0 }; -} - -function getGeminiCliThinkingLevel(effort: ClampedThinkingLevel, modelId: string): GoogleThinkingLevel { - if (isGemini3ProModel(modelId)) { - switch (effort) { - case "minimal": - case "low": - return "LOW"; - case "medium": - case "high": - return "HIGH"; - } - } - switch (effort) { - case "minimal": - return "MINIMAL"; - case "low": - return "LOW"; - case "medium": - return "MEDIUM"; - case "high": - return "HIGH"; - } -} diff --git a/packages/ai/src/providers/google-shared.ts b/packages/ai/src/providers/google-shared.ts index e5adfb95..f03a8377 100644 --- a/packages/ai/src/providers/google-shared.ts +++ b/packages/ai/src/providers/google-shared.ts @@ -1,5 +1,5 @@ /** - * Shared utilities for Google Generative AI and Google Cloud Code Assist providers. + * Shared utilities for Google Generative AI and Google Vertex providers. */ import { type Content, FinishReason, FunctionCallingConfigMode, type Part } from "@google/genai"; @@ -7,7 +7,13 @@ import type { Context, ImageContent, Model, StopReason, TextContent, Tool } from import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; import { transformMessages } from "./transform-messages.js"; -type GoogleApiType = "google-generative-ai" | "google-gemini-cli" | "google-vertex"; +type GoogleApiType = "google-generative-ai" | "google-vertex"; + +/** + * Thinking level for Gemini 3 models. + * Mirrors Google's ThinkingLevel enum values. + */ +export type GoogleThinkingLevel = "THINKING_LEVEL_UNSPECIFIED" | "MINIMAL" | "LOW" | "MEDIUM" | "HIGH"; /** * Determines whether a streamed Gemini `Part` should be treated as "thinking". @@ -129,7 +135,7 @@ export function convertMessages(model: Model, contex for (const block of msg.content) { if (block.type === "text") { - // Skip empty text blocks - they can cause issues with some models (e.g. Claude via Antigravity) + // Skip empty text blocks if (!block.text || block.text.trim() === "") continue; const thoughtSignature = resolveThoughtSignature(isSameProviderAndModel, block.textSignature); parts.push({ @@ -157,7 +163,7 @@ export function convertMessages(model: Model, contex const thoughtSignature = resolveThoughtSignature(isSameProviderAndModel, block.thoughtSignature); // Gemini 3 requires thoughtSignature on all function calls when thinking mode is enabled. // Use the skip_thought_signature_validator sentinel for unsigned function calls - // (e.g. replayed from providers without thought signatures like Claude via Antigravity). + // when replayed from providers without thought signatures. const isGemini3 = model.id.toLowerCase().includes("gemini-3"); const effectiveSignature = thoughtSignature || (isGemini3 ? SKIP_THOUGHT_SIGNATURE : undefined); const part: Part = { @@ -190,7 +196,7 @@ export function convertMessages(model: Model, contex // Gemini 3+ models support multimodal function responses with images nested inside // functionResponse.parts. Claude and other non-Gemini models behind Cloud Code Assist / - // Antigravity also accept this shape. Gemini < 3 still needs a separate user image turn. + // Gemini < 3 still needs a separate user image turn. const modelSupportsMultimodalFunctionResponse = supportsMultimodalFunctionResponse(model.id); // Use "output" key for success, "error" key for errors as per SDK documentation diff --git a/packages/ai/src/providers/google-vertex.ts b/packages/ai/src/providers/google-vertex.ts index 739fff27..b8bc1ded 100644 --- a/packages/ai/src/providers/google-vertex.ts +++ b/packages/ai/src/providers/google-vertex.ts @@ -24,7 +24,7 @@ import type { } from "../types.js"; import { AssistantMessageEventStream } from "../utils/event-stream.js"; import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; -import type { GoogleThinkingLevel } from "./google-gemini-cli.js"; +import type { GoogleThinkingLevel } from "./google-shared.js"; import { convertMessages, convertTools, diff --git a/packages/ai/src/providers/google.ts b/packages/ai/src/providers/google.ts index 46cc5ba6..9d69e65d 100644 --- a/packages/ai/src/providers/google.ts +++ b/packages/ai/src/providers/google.ts @@ -22,7 +22,7 @@ import type { } from "../types.js"; import { AssistantMessageEventStream } from "../utils/event-stream.js"; import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; -import type { GoogleThinkingLevel } from "./google-gemini-cli.js"; +import type { GoogleThinkingLevel } from "./google-shared.js"; import { convertMessages, convertTools, diff --git a/packages/ai/src/providers/register-builtins.ts b/packages/ai/src/providers/register-builtins.ts index 2082be2f..64475680 100644 --- a/packages/ai/src/providers/register-builtins.ts +++ b/packages/ai/src/providers/register-builtins.ts @@ -14,7 +14,6 @@ import type { BedrockOptions } from "./amazon-bedrock.js"; import type { AnthropicOptions } from "./anthropic.js"; import type { AzureOpenAIResponsesOptions } from "./azure-openai-responses.js"; import type { GoogleOptions } from "./google.js"; -import type { GoogleGeminiCliOptions } from "./google-gemini-cli.js"; import type { GoogleVertexOptions } from "./google-vertex.js"; import type { MistralOptions } from "./mistral.js"; import type { OpenAICodexResponsesOptions } from "./openai-codex-responses.js"; @@ -49,11 +48,6 @@ interface GoogleProviderModule { streamSimpleGoogle: StreamFunction<"google-generative-ai", SimpleStreamOptions>; } -interface GoogleGeminiCliProviderModule { - streamGoogleGeminiCli: StreamFunction<"google-gemini-cli", GoogleGeminiCliOptions>; - streamSimpleGoogleGeminiCli: StreamFunction<"google-gemini-cli", SimpleStreamOptions>; -} - interface GoogleVertexProviderModule { streamGoogleVertex: StreamFunction<"google-vertex", GoogleVertexOptions>; streamSimpleGoogleVertex: StreamFunction<"google-vertex", SimpleStreamOptions>; @@ -103,9 +97,6 @@ let azureOpenAIResponsesProviderModulePromise: let googleProviderModulePromise: | Promise> | undefined; -let googleGeminiCliProviderModulePromise: - | Promise> - | undefined; let googleVertexProviderModulePromise: | Promise> | undefined; @@ -248,19 +239,6 @@ function loadGoogleProviderModule(): Promise< return googleProviderModulePromise; } -function loadGoogleGeminiCliProviderModule(): Promise< - LazyProviderModule<"google-gemini-cli", GoogleGeminiCliOptions, SimpleStreamOptions> -> { - googleGeminiCliProviderModulePromise ||= import("./google-gemini-cli.js").then((module) => { - const provider = module as GoogleGeminiCliProviderModule; - return { - stream: provider.streamGoogleGeminiCli, - streamSimple: provider.streamSimpleGoogleGeminiCli, - }; - }); - return googleGeminiCliProviderModulePromise; -} - function loadGoogleVertexProviderModule(): Promise< LazyProviderModule<"google-vertex", GoogleVertexOptions, SimpleStreamOptions> > { @@ -348,8 +326,6 @@ export const streamAzureOpenAIResponses = createLazyStream(loadAzureOpenAIRespon export const streamSimpleAzureOpenAIResponses = createLazySimpleStream(loadAzureOpenAIResponsesProviderModule); export const streamGoogle = createLazyStream(loadGoogleProviderModule); export const streamSimpleGoogle = createLazySimpleStream(loadGoogleProviderModule); -export const streamGoogleGeminiCli = createLazyStream(loadGoogleGeminiCliProviderModule); -export const streamSimpleGoogleGeminiCli = createLazySimpleStream(loadGoogleGeminiCliProviderModule); export const streamGoogleVertex = createLazyStream(loadGoogleVertexProviderModule); export const streamSimpleGoogleVertex = createLazySimpleStream(loadGoogleVertexProviderModule); export const streamMistral = createLazyStream(loadMistralProviderModule); @@ -406,12 +382,6 @@ export function registerBuiltInApiProviders(): void { streamSimple: streamSimpleGoogle, }); - registerApiProvider({ - api: "google-gemini-cli", - stream: streamGoogleGeminiCli, - streamSimple: streamSimpleGoogleGeminiCli, - }); - registerApiProvider({ api: "google-vertex", stream: streamGoogleVertex, diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index 4befe310..4c6af7f9 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -11,7 +11,6 @@ export type KnownApi = | "anthropic-messages" | "bedrock-converse-stream" | "google-generative-ai" - | "google-gemini-cli" | "google-vertex"; export type Api = KnownApi | (string & {}); @@ -20,8 +19,6 @@ export type KnownProvider = | "amazon-bedrock" | "anthropic" | "google" - | "google-gemini-cli" - | "google-antigravity" | "google-vertex" | "openai" | "azure-openai-responses" diff --git a/packages/ai/src/utils/oauth/google-antigravity.ts b/packages/ai/src/utils/oauth/google-antigravity.ts deleted file mode 100644 index d5843e32..00000000 --- a/packages/ai/src/utils/oauth/google-antigravity.ts +++ /dev/null @@ -1,455 +0,0 @@ -/** - * Antigravity OAuth flow (Gemini 3, Claude, GPT-OSS via Google Cloud) - * Uses different OAuth credentials than google-gemini-cli for access to additional models. - * - * NOTE: This module uses Node.js http.createServer for the OAuth callback. - * It is only intended for CLI use, not browser environments. - */ - -import type { Server } from "node:http"; -import { oauthErrorHtml, oauthSuccessHtml } from "./oauth-page.js"; -import { generatePKCE } from "./pkce.js"; -import type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from "./types.js"; - -type AntigravityCredentials = OAuthCredentials & { - projectId: string; -}; - -const CALLBACK_HOST = process.env.PI_OAUTH_CALLBACK_HOST || "127.0.0.1"; - -let _createServer: typeof import("node:http").createServer | null = null; -let _httpImportPromise: Promise | null = null; -if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) { - _httpImportPromise = import("node:http").then((m) => { - _createServer = m.createServer; - }); -} - -// Antigravity OAuth credentials (different from Gemini CLI) -const decode = (s: string) => atob(s); -const CLIENT_ID = decode( - "MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ==", -); -const CLIENT_SECRET = decode("R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY="); -const REDIRECT_URI = "http://localhost:51121/oauth-callback"; - -// Antigravity requires additional scopes -const SCOPES = [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/userinfo.email", - "https://www.googleapis.com/auth/userinfo.profile", - "https://www.googleapis.com/auth/cclog", - "https://www.googleapis.com/auth/experimentsandconfigs", -]; - -const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"; -const TOKEN_URL = "https://oauth2.googleapis.com/token"; - -// Fallback project ID when discovery fails -const DEFAULT_PROJECT_ID = "rising-fact-p41fc"; - -type CallbackServerInfo = { - server: Server; - cancelWait: () => void; - waitForCode: () => Promise<{ code: string; state: string } | null>; -}; - -/** - * Start a local HTTP server to receive the OAuth callback - */ -async function getNodeCreateServer(): Promise { - if (_createServer) return _createServer; - if (_httpImportPromise) { - await _httpImportPromise; - } - if (_createServer) return _createServer; - throw new Error("Antigravity OAuth is only available in Node.js environments"); -} - -async function startCallbackServer(): Promise { - const createServer = await getNodeCreateServer(); - - return new Promise((resolve, reject) => { - let settleWait: ((value: { code: string; state: string } | null) => void) | undefined; - const waitForCodePromise = new Promise<{ code: string; state: string } | null>((resolveWait) => { - let settled = false; - settleWait = (value) => { - if (settled) return; - settled = true; - resolveWait(value); - }; - }); - - const server = createServer((req, res) => { - const url = new URL(req.url || "", `http://localhost:51121`); - - if (url.pathname === "/oauth-callback") { - const code = url.searchParams.get("code"); - const state = url.searchParams.get("state"); - const error = url.searchParams.get("error"); - - if (error) { - res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" }); - res.end(oauthErrorHtml("Google authentication did not complete.", `Error: ${error}`)); - return; - } - - if (code && state) { - res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); - res.end(oauthSuccessHtml("Google authentication completed. You can close this window.")); - settleWait?.({ code, state }); - } else { - res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" }); - res.end(oauthErrorHtml("Missing code or state parameter.")); - } - } else { - res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" }); - res.end(oauthErrorHtml("Callback route not found.")); - } - }); - - server.on("error", (err) => { - reject(err); - }); - - server.listen(51121, CALLBACK_HOST, () => { - resolve({ - server, - cancelWait: () => { - settleWait?.(null); - }, - waitForCode: () => waitForCodePromise, - }); - }); - }); -} - -/** - * Parse redirect URL to extract code and state - */ -function parseRedirectUrl(input: string): { code?: string; state?: string } { - const value = input.trim(); - if (!value) return {}; - - try { - const url = new URL(value); - return { - code: url.searchParams.get("code") ?? undefined, - state: url.searchParams.get("state") ?? undefined, - }; - } catch { - // Not a URL, return empty - return {}; - } -} - -interface LoadCodeAssistPayload { - cloudaicompanionProject?: string | { id?: string }; - currentTier?: { id?: string }; - allowedTiers?: Array<{ id?: string; isDefault?: boolean }>; -} - -/** - * Discover or provision a project for the user - */ -async function discoverProject(accessToken: string, onProgress?: (message: string) => void): Promise { - const headers = { - Authorization: `Bearer ${accessToken}`, - "Content-Type": "application/json", - "User-Agent": "google-api-nodejs-client/9.15.1", - "X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1", - "Client-Metadata": JSON.stringify({ - ideType: "IDE_UNSPECIFIED", - platform: "PLATFORM_UNSPECIFIED", - pluginType: "GEMINI", - }), - }; - - // Try endpoints in order: prod first, then sandbox - const endpoints = ["https://cloudcode-pa.googleapis.com", "https://daily-cloudcode-pa.sandbox.googleapis.com"]; - - onProgress?.("Checking for existing project..."); - - for (const endpoint of endpoints) { - try { - const loadResponse = await fetch(`${endpoint}/v1internal:loadCodeAssist`, { - method: "POST", - headers, - body: JSON.stringify({ - metadata: { - ideType: "IDE_UNSPECIFIED", - platform: "PLATFORM_UNSPECIFIED", - pluginType: "GEMINI", - }, - }), - }); - - if (loadResponse.ok) { - const data = (await loadResponse.json()) as LoadCodeAssistPayload; - - // Handle both string and object formats - if (typeof data.cloudaicompanionProject === "string" && data.cloudaicompanionProject) { - return data.cloudaicompanionProject; - } - if ( - data.cloudaicompanionProject && - typeof data.cloudaicompanionProject === "object" && - data.cloudaicompanionProject.id - ) { - return data.cloudaicompanionProject.id; - } - } - } catch { - // Try next endpoint - } - } - - // Use fallback project ID - onProgress?.("Using default project..."); - return DEFAULT_PROJECT_ID; -} - -/** - * Get user email from the access token - */ -async function getUserEmail(accessToken: string): Promise { - try { - const response = await fetch("https://www.googleapis.com/oauth2/v1/userinfo?alt=json", { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }); - - if (response.ok) { - const data = (await response.json()) as { email?: string }; - return data.email; - } - } catch { - // Ignore errors, email is optional - } - return undefined; -} - -/** - * Refresh Antigravity token - */ -export async function refreshAntigravityToken(refreshToken: string, projectId: string): Promise { - const response = await fetch(TOKEN_URL, { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: new URLSearchParams({ - client_id: CLIENT_ID, - client_secret: CLIENT_SECRET, - refresh_token: refreshToken, - grant_type: "refresh_token", - }), - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`Antigravity token refresh failed: ${error}`); - } - - const data = (await response.json()) as { - access_token: string; - expires_in: number; - refresh_token?: string; - }; - - return { - refresh: data.refresh_token || refreshToken, - access: data.access_token, - expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000, - projectId, - }; -} - -/** - * Login with Antigravity OAuth - * - * @param onAuth - Callback with URL and optional instructions - * @param onProgress - Optional progress callback - * @param onManualCodeInput - Optional promise that resolves with user-pasted redirect URL. - * Races with browser callback - whichever completes first wins. - */ -export async function loginAntigravity( - onAuth: (info: { url: string; instructions?: string }) => void, - onProgress?: (message: string) => void, - onManualCodeInput?: () => Promise, -): Promise { - const { verifier, challenge } = await generatePKCE(); - - // Start local server for callback - onProgress?.("Starting local server for OAuth callback..."); - const server = await startCallbackServer(); - - let code: string | undefined; - - try { - // Build authorization URL - const authParams = new URLSearchParams({ - client_id: CLIENT_ID, - response_type: "code", - redirect_uri: REDIRECT_URI, - scope: SCOPES.join(" "), - code_challenge: challenge, - code_challenge_method: "S256", - state: verifier, - access_type: "offline", - prompt: "consent", - }); - - const authUrl = `${AUTH_URL}?${authParams.toString()}`; - - // Notify caller with URL to open - onAuth({ - url: authUrl, - instructions: "Complete the sign-in in your browser.", - }); - - // Wait for the callback, racing with manual input if provided - onProgress?.("Waiting for OAuth callback..."); - - if (onManualCodeInput) { - // Race between browser callback and manual input - let manualInput: string | undefined; - let manualError: Error | undefined; - const manualPromise = onManualCodeInput() - .then((input) => { - manualInput = input; - server.cancelWait(); - }) - .catch((err) => { - manualError = err instanceof Error ? err : new Error(String(err)); - server.cancelWait(); - }); - - const result = await server.waitForCode(); - - // If manual input was cancelled, throw that error - if (manualError) { - throw manualError; - } - - if (result?.code) { - // Browser callback won - verify state - if (result.state !== verifier) { - throw new Error("OAuth state mismatch - possible CSRF attack"); - } - code = result.code; - } else if (manualInput) { - // Manual input won - const parsed = parseRedirectUrl(manualInput); - if (parsed.state && parsed.state !== verifier) { - throw new Error("OAuth state mismatch - possible CSRF attack"); - } - code = parsed.code; - } - - // If still no code, wait for manual promise and try that - if (!code) { - await manualPromise; - if (manualError) { - throw manualError; - } - if (manualInput) { - const parsed = parseRedirectUrl(manualInput); - if (parsed.state && parsed.state !== verifier) { - throw new Error("OAuth state mismatch - possible CSRF attack"); - } - code = parsed.code; - } - } - } else { - // Original flow: just wait for callback - const result = await server.waitForCode(); - if (result?.code) { - if (result.state !== verifier) { - throw new Error("OAuth state mismatch - possible CSRF attack"); - } - code = result.code; - } - } - - if (!code) { - throw new Error("No authorization code received"); - } - - // Exchange code for tokens - onProgress?.("Exchanging authorization code for tokens..."); - const tokenResponse = await fetch(TOKEN_URL, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - client_id: CLIENT_ID, - client_secret: CLIENT_SECRET, - code, - grant_type: "authorization_code", - redirect_uri: REDIRECT_URI, - code_verifier: verifier, - }), - }); - - if (!tokenResponse.ok) { - const error = await tokenResponse.text(); - throw new Error(`Token exchange failed: ${error}`); - } - - const tokenData = (await tokenResponse.json()) as { - access_token: string; - refresh_token: string; - expires_in: number; - }; - - if (!tokenData.refresh_token) { - throw new Error("No refresh token received. Please try again."); - } - - // Get user email - onProgress?.("Getting user info..."); - const email = await getUserEmail(tokenData.access_token); - - // Discover project - const projectId = await discoverProject(tokenData.access_token, onProgress); - - // Calculate expiry time (current time + expires_in seconds - 5 min buffer) - const expiresAt = Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000; - - const credentials: OAuthCredentials = { - refresh: tokenData.refresh_token, - access: tokenData.access_token, - expires: expiresAt, - projectId, - email, - }; - - return credentials; - } finally { - server.server.close(); - } -} - -export const antigravityOAuthProvider: OAuthProviderInterface = { - id: "google-antigravity", - name: "Antigravity (Gemini 3, Claude, GPT-OSS)", - usesCallbackServer: true, - - async login(callbacks: OAuthLoginCallbacks): Promise { - return loginAntigravity(callbacks.onAuth, callbacks.onProgress, callbacks.onManualCodeInput); - }, - - async refreshToken(credentials: OAuthCredentials): Promise { - const creds = credentials as AntigravityCredentials; - if (!creds.projectId) { - throw new Error("Antigravity credentials missing projectId"); - } - return refreshAntigravityToken(creds.refresh, creds.projectId); - }, - - getApiKey(credentials: OAuthCredentials): string { - const creds = credentials as AntigravityCredentials; - return JSON.stringify({ token: creds.access, projectId: creds.projectId }); - }, -}; diff --git a/packages/ai/src/utils/oauth/google-gemini-cli.ts b/packages/ai/src/utils/oauth/google-gemini-cli.ts deleted file mode 100644 index ad4f3991..00000000 --- a/packages/ai/src/utils/oauth/google-gemini-cli.ts +++ /dev/null @@ -1,597 +0,0 @@ -/** - * Gemini CLI OAuth flow (Google Cloud Code Assist) - * Standard Gemini models only (gemini-2.0-flash, gemini-2.5-*) - * - * NOTE: This module uses Node.js http.createServer for the OAuth callback. - * It is only intended for CLI use, not browser environments. - */ - -import type { Server } from "node:http"; -import { oauthErrorHtml, oauthSuccessHtml } from "./oauth-page.js"; -import { generatePKCE } from "./pkce.js"; -import type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from "./types.js"; - -type GeminiCredentials = OAuthCredentials & { - projectId: string; -}; - -const CALLBACK_HOST = process.env.PI_OAUTH_CALLBACK_HOST || "127.0.0.1"; - -let _createServer: typeof import("node:http").createServer | null = null; -let _httpImportPromise: Promise | null = null; -if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) { - _httpImportPromise = import("node:http").then((m) => { - _createServer = m.createServer; - }); -} - -const decode = (s: string) => atob(s); -const CLIENT_ID = decode( - "NjgxMjU1ODA5Mzk1LW9vOGZ0Mm9wcmRybnA5ZTNhcWY2YXYzaG1kaWIxMzVqLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29t", -); -const CLIENT_SECRET = decode("R09DU1BYLTR1SGdNUG0tMW83U2stZ2VWNkN1NWNsWEZzeGw="); -const REDIRECT_URI = "http://localhost:8085/oauth2callback"; -const SCOPES = [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/userinfo.email", - "https://www.googleapis.com/auth/userinfo.profile", -]; -const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"; -const TOKEN_URL = "https://oauth2.googleapis.com/token"; -const CODE_ASSIST_ENDPOINT = "https://cloudcode-pa.googleapis.com"; - -type CallbackServerInfo = { - server: Server; - cancelWait: () => void; - waitForCode: () => Promise<{ code: string; state: string } | null>; -}; - -/** - * Start a local HTTP server to receive the OAuth callback - */ -async function getNodeCreateServer(): Promise { - if (_createServer) return _createServer; - if (_httpImportPromise) { - await _httpImportPromise; - } - if (_createServer) return _createServer; - throw new Error("Gemini CLI OAuth is only available in Node.js environments"); -} - -async function startCallbackServer(): Promise { - const createServer = await getNodeCreateServer(); - - return new Promise((resolve, reject) => { - let settleWait: ((value: { code: string; state: string } | null) => void) | undefined; - const waitForCodePromise = new Promise<{ code: string; state: string } | null>((resolveWait) => { - let settled = false; - settleWait = (value) => { - if (settled) return; - settled = true; - resolveWait(value); - }; - }); - - const server = createServer((req, res) => { - const url = new URL(req.url || "", `http://localhost:8085`); - - if (url.pathname === "/oauth2callback") { - const code = url.searchParams.get("code"); - const state = url.searchParams.get("state"); - const error = url.searchParams.get("error"); - - if (error) { - res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" }); - res.end(oauthErrorHtml("Google authentication did not complete.", `Error: ${error}`)); - return; - } - - if (code && state) { - res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); - res.end(oauthSuccessHtml("Google authentication completed. You can close this window.")); - settleWait?.({ code, state }); - } else { - res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" }); - res.end(oauthErrorHtml("Missing code or state parameter.")); - } - } else { - res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" }); - res.end(oauthErrorHtml("Callback route not found.")); - } - }); - - server.on("error", (err) => { - reject(err); - }); - - server.listen(8085, CALLBACK_HOST, () => { - resolve({ - server, - cancelWait: () => { - settleWait?.(null); - }, - waitForCode: () => waitForCodePromise, - }); - }); - }); -} - -/** - * Parse redirect URL to extract code and state - */ -function parseRedirectUrl(input: string): { code?: string; state?: string } { - const value = input.trim(); - if (!value) return {}; - - try { - const url = new URL(value); - return { - code: url.searchParams.get("code") ?? undefined, - state: url.searchParams.get("state") ?? undefined, - }; - } catch { - // Not a URL, return empty - return {}; - } -} - -interface LoadCodeAssistPayload { - cloudaicompanionProject?: string; - currentTier?: { id?: string }; - allowedTiers?: Array<{ id?: string; isDefault?: boolean }>; -} - -/** - * Long-running operation response from onboardUser - */ -interface LongRunningOperationResponse { - name?: string; - done?: boolean; - response?: { - cloudaicompanionProject?: { id?: string }; - }; -} - -// Tier IDs as used by the Cloud Code API -const TIER_FREE = "free-tier"; -const TIER_LEGACY = "legacy-tier"; -const TIER_STANDARD = "standard-tier"; - -interface GoogleRpcErrorResponse { - error?: { - details?: Array<{ reason?: string }>; - }; -} - -/** - * Wait helper for onboarding retries - */ -function wait(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -/** - * Get default tier from allowed tiers - */ -function getDefaultTier(allowedTiers?: Array<{ id?: string; isDefault?: boolean }>): { id?: string } { - if (!allowedTiers || allowedTiers.length === 0) return { id: TIER_LEGACY }; - const defaultTier = allowedTiers.find((t) => t.isDefault); - return defaultTier ?? { id: TIER_LEGACY }; -} - -function isVpcScAffectedUser(payload: unknown): boolean { - if (!payload || typeof payload !== "object") return false; - if (!("error" in payload)) return false; - const error = (payload as GoogleRpcErrorResponse).error; - if (!error?.details || !Array.isArray(error.details)) return false; - return error.details.some((detail) => detail.reason === "SECURITY_POLICY_VIOLATED"); -} - -/** - * Poll a long-running operation until completion - */ -async function pollOperation( - operationName: string, - headers: Record, - onProgress?: (message: string) => void, -): Promise { - let attempt = 0; - while (true) { - if (attempt > 0) { - onProgress?.(`Waiting for project provisioning (attempt ${attempt + 1})...`); - await wait(5000); - } - - const response = await fetch(`${CODE_ASSIST_ENDPOINT}/v1internal/${operationName}`, { - method: "GET", - headers, - }); - - if (!response.ok) { - throw new Error(`Failed to poll operation: ${response.status} ${response.statusText}`); - } - - const data = (await response.json()) as LongRunningOperationResponse; - if (data.done) { - return data; - } - - attempt += 1; - } -} - -/** - * Discover or provision a Google Cloud project for the user - */ -async function discoverProject(accessToken: string, onProgress?: (message: string) => void): Promise { - // Check for user-provided project ID via environment variable - const envProjectId = process.env.GOOGLE_CLOUD_PROJECT || process.env.GOOGLE_CLOUD_PROJECT_ID; - - const headers = { - Authorization: `Bearer ${accessToken}`, - "Content-Type": "application/json", - "User-Agent": "google-api-nodejs-client/9.15.1", - "X-Goog-Api-Client": "gl-node/22.17.0", - }; - - // Try to load existing project via loadCodeAssist - onProgress?.("Checking for existing Cloud Code Assist project..."); - const loadResponse = await fetch(`${CODE_ASSIST_ENDPOINT}/v1internal:loadCodeAssist`, { - method: "POST", - headers, - body: JSON.stringify({ - cloudaicompanionProject: envProjectId, - metadata: { - ideType: "IDE_UNSPECIFIED", - platform: "PLATFORM_UNSPECIFIED", - pluginType: "GEMINI", - duetProject: envProjectId, - }, - }), - }); - - let data: LoadCodeAssistPayload; - - if (!loadResponse.ok) { - let errorPayload: unknown; - try { - errorPayload = await loadResponse.clone().json(); - } catch { - errorPayload = undefined; - } - - if (isVpcScAffectedUser(errorPayload)) { - data = { currentTier: { id: TIER_STANDARD } }; - } else { - const errorText = await loadResponse.text(); - throw new Error(`loadCodeAssist failed: ${loadResponse.status} ${loadResponse.statusText}: ${errorText}`); - } - } else { - data = (await loadResponse.json()) as LoadCodeAssistPayload; - } - - // If user already has a current tier and project, use it - if (data.currentTier) { - if (data.cloudaicompanionProject) { - return data.cloudaicompanionProject; - } - // User has a tier but no managed project - they need to provide one via env var - if (envProjectId) { - return envProjectId; - } - throw new Error( - "This account requires setting the GOOGLE_CLOUD_PROJECT or GOOGLE_CLOUD_PROJECT_ID environment variable. " + - "See https://goo.gle/gemini-cli-auth-docs#workspace-gca", - ); - } - - // User needs to be onboarded - get the default tier - const tier = getDefaultTier(data.allowedTiers); - const tierId = tier?.id ?? TIER_FREE; - - if (tierId !== TIER_FREE && !envProjectId) { - throw new Error( - "This account requires setting the GOOGLE_CLOUD_PROJECT or GOOGLE_CLOUD_PROJECT_ID environment variable. " + - "See https://goo.gle/gemini-cli-auth-docs#workspace-gca", - ); - } - - onProgress?.("Provisioning Cloud Code Assist project (this may take a moment)..."); - - // Build onboard request - for free tier, don't include project ID (Google provisions one) - // For other tiers, include the user's project ID if available - const onboardBody: Record = { - tierId, - metadata: { - ideType: "IDE_UNSPECIFIED", - platform: "PLATFORM_UNSPECIFIED", - pluginType: "GEMINI", - }, - }; - - if (tierId !== TIER_FREE && envProjectId) { - onboardBody.cloudaicompanionProject = envProjectId; - (onboardBody.metadata as Record).duetProject = envProjectId; - } - - // Start onboarding - this returns a long-running operation - const onboardResponse = await fetch(`${CODE_ASSIST_ENDPOINT}/v1internal:onboardUser`, { - method: "POST", - headers, - body: JSON.stringify(onboardBody), - }); - - if (!onboardResponse.ok) { - const errorText = await onboardResponse.text(); - throw new Error(`onboardUser failed: ${onboardResponse.status} ${onboardResponse.statusText}: ${errorText}`); - } - - let lroData = (await onboardResponse.json()) as LongRunningOperationResponse; - - // If the operation isn't done yet, poll until completion - if (!lroData.done && lroData.name) { - lroData = await pollOperation(lroData.name, headers, onProgress); - } - - // Try to get project ID from the response - const projectId = lroData.response?.cloudaicompanionProject?.id; - if (projectId) { - return projectId; - } - - // If no project ID from onboarding, fall back to env var - if (envProjectId) { - return envProjectId; - } - - throw new Error( - "Could not discover or provision a Google Cloud project. " + - "Try setting the GOOGLE_CLOUD_PROJECT or GOOGLE_CLOUD_PROJECT_ID environment variable. " + - "See https://goo.gle/gemini-cli-auth-docs#workspace-gca", - ); -} - -/** - * Get user email from the access token - */ -async function getUserEmail(accessToken: string): Promise { - try { - const response = await fetch("https://www.googleapis.com/oauth2/v1/userinfo?alt=json", { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }); - - if (response.ok) { - const data = (await response.json()) as { email?: string }; - return data.email; - } - } catch { - // Ignore errors, email is optional - } - return undefined; -} - -/** - * Refresh Google Cloud Code Assist token - */ -export async function refreshGoogleCloudToken(refreshToken: string, projectId: string): Promise { - const response = await fetch(TOKEN_URL, { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: new URLSearchParams({ - client_id: CLIENT_ID, - client_secret: CLIENT_SECRET, - refresh_token: refreshToken, - grant_type: "refresh_token", - }), - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`Google Cloud token refresh failed: ${error}`); - } - - const data = (await response.json()) as { - access_token: string; - expires_in: number; - refresh_token?: string; - }; - - return { - refresh: data.refresh_token || refreshToken, - access: data.access_token, - expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000, - projectId, - }; -} - -/** - * Login with Gemini CLI (Google Cloud Code Assist) OAuth - * - * @param onAuth - Callback with URL and optional instructions - * @param onProgress - Optional progress callback - * @param onManualCodeInput - Optional promise that resolves with user-pasted redirect URL. - * Races with browser callback - whichever completes first wins. - */ -export async function loginGeminiCli( - onAuth: (info: { url: string; instructions?: string }) => void, - onProgress?: (message: string) => void, - onManualCodeInput?: () => Promise, -): Promise { - const { verifier, challenge } = await generatePKCE(); - - // Start local server for callback - onProgress?.("Starting local server for OAuth callback..."); - const server = await startCallbackServer(); - - let code: string | undefined; - - try { - // Build authorization URL - const authParams = new URLSearchParams({ - client_id: CLIENT_ID, - response_type: "code", - redirect_uri: REDIRECT_URI, - scope: SCOPES.join(" "), - code_challenge: challenge, - code_challenge_method: "S256", - state: verifier, - access_type: "offline", - prompt: "consent", - }); - - const authUrl = `${AUTH_URL}?${authParams.toString()}`; - - // Notify caller with URL to open - onAuth({ - url: authUrl, - instructions: "Complete the sign-in in your browser.", - }); - - // Wait for the callback, racing with manual input if provided - onProgress?.("Waiting for OAuth callback..."); - - if (onManualCodeInput) { - // Race between browser callback and manual input - let manualInput: string | undefined; - let manualError: Error | undefined; - const manualPromise = onManualCodeInput() - .then((input) => { - manualInput = input; - server.cancelWait(); - }) - .catch((err) => { - manualError = err instanceof Error ? err : new Error(String(err)); - server.cancelWait(); - }); - - const result = await server.waitForCode(); - - // If manual input was cancelled, throw that error - if (manualError) { - throw manualError; - } - - if (result?.code) { - // Browser callback won - verify state - if (result.state !== verifier) { - throw new Error("OAuth state mismatch - possible CSRF attack"); - } - code = result.code; - } else if (manualInput) { - // Manual input won - const parsed = parseRedirectUrl(manualInput); - if (parsed.state && parsed.state !== verifier) { - throw new Error("OAuth state mismatch - possible CSRF attack"); - } - code = parsed.code; - } - - // If still no code, wait for manual promise and try that - if (!code) { - await manualPromise; - if (manualError) { - throw manualError; - } - if (manualInput) { - const parsed = parseRedirectUrl(manualInput); - if (parsed.state && parsed.state !== verifier) { - throw new Error("OAuth state mismatch - possible CSRF attack"); - } - code = parsed.code; - } - } - } else { - // Original flow: just wait for callback - const result = await server.waitForCode(); - if (result?.code) { - if (result.state !== verifier) { - throw new Error("OAuth state mismatch - possible CSRF attack"); - } - code = result.code; - } - } - - if (!code) { - throw new Error("No authorization code received"); - } - - // Exchange code for tokens - onProgress?.("Exchanging authorization code for tokens..."); - const tokenResponse = await fetch(TOKEN_URL, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - client_id: CLIENT_ID, - client_secret: CLIENT_SECRET, - code, - grant_type: "authorization_code", - redirect_uri: REDIRECT_URI, - code_verifier: verifier, - }), - }); - - if (!tokenResponse.ok) { - const error = await tokenResponse.text(); - throw new Error(`Token exchange failed: ${error}`); - } - - const tokenData = (await tokenResponse.json()) as { - access_token: string; - refresh_token: string; - expires_in: number; - }; - - if (!tokenData.refresh_token) { - throw new Error("No refresh token received. Please try again."); - } - - // Get user email - onProgress?.("Getting user info..."); - const email = await getUserEmail(tokenData.access_token); - - // Discover project - const projectId = await discoverProject(tokenData.access_token, onProgress); - - // Calculate expiry time (current time + expires_in seconds - 5 min buffer) - const expiresAt = Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000; - - const credentials: OAuthCredentials = { - refresh: tokenData.refresh_token, - access: tokenData.access_token, - expires: expiresAt, - projectId, - email, - }; - - return credentials; - } finally { - server.server.close(); - } -} - -export const geminiCliOAuthProvider: OAuthProviderInterface = { - id: "google-gemini-cli", - name: "Google Cloud Code Assist (Gemini CLI)", - usesCallbackServer: true, - - async login(callbacks: OAuthLoginCallbacks): Promise { - return loginGeminiCli(callbacks.onAuth, callbacks.onProgress, callbacks.onManualCodeInput); - }, - - async refreshToken(credentials: OAuthCredentials): Promise { - const creds = credentials as GeminiCredentials; - if (!creds.projectId) { - throw new Error("Google Cloud credentials missing projectId"); - } - return refreshGoogleCloudToken(creds.refresh, creds.projectId); - }, - - getApiKey(credentials: OAuthCredentials): string { - const creds = credentials as GeminiCredentials; - return JSON.stringify({ token: creds.access, projectId: creds.projectId }); - }, -}; diff --git a/packages/ai/src/utils/oauth/index.ts b/packages/ai/src/utils/oauth/index.ts index 256562a8..f5f180bf 100644 --- a/packages/ai/src/utils/oauth/index.ts +++ b/packages/ai/src/utils/oauth/index.ts @@ -5,8 +5,6 @@ * for OAuth-based providers: * - Anthropic (Claude Pro/Max) * - GitHub Copilot - * - Google Cloud Code Assist (Gemini CLI) - * - Antigravity (Gemini 3, Claude, GPT-OSS via Google Cloud) */ // Anthropic @@ -19,10 +17,6 @@ export { normalizeDomain, refreshGitHubCopilotToken, } from "./github-copilot.js"; -// Google Antigravity -export { antigravityOAuthProvider, loginAntigravity, refreshAntigravityToken } from "./google-antigravity.js"; -// Google Gemini CLI -export { geminiCliOAuthProvider, loginGeminiCli, refreshGoogleCloudToken } from "./google-gemini-cli.js"; // OpenAI Codex (ChatGPT OAuth) export { loginOpenAICodex, openaiCodexOAuthProvider, refreshOpenAICodexToken } from "./openai-codex.js"; @@ -34,16 +28,12 @@ export * from "./types.js"; import { anthropicOAuthProvider } from "./anthropic.js"; import { githubCopilotOAuthProvider } from "./github-copilot.js"; -import { antigravityOAuthProvider } from "./google-antigravity.js"; -import { geminiCliOAuthProvider } from "./google-gemini-cli.js"; import { openaiCodexOAuthProvider } from "./openai-codex.js"; import type { OAuthCredentials, OAuthProviderId, OAuthProviderInfo, OAuthProviderInterface } from "./types.js"; const BUILT_IN_OAUTH_PROVIDERS: OAuthProviderInterface[] = [ anthropicOAuthProvider, githubCopilotOAuthProvider, - geminiCliOAuthProvider, - antigravityOAuthProvider, openaiCodexOAuthProvider, ]; diff --git a/packages/ai/test/abort.test.ts b/packages/ai/test/abort.test.ts index 924d0821..8a7fac4b 100644 --- a/packages/ai/test/abort.test.ts +++ b/packages/ai/test/abort.test.ts @@ -10,10 +10,7 @@ import { hasBedrockCredentials } from "./bedrock-utils.js"; import { resolveApiKey } from "./oauth.js"; // Resolve OAuth tokens at module level (async, runs before tests) -const [geminiCliToken, openaiCodexToken] = await Promise.all([ - resolveApiKey("google-gemini-cli"), - resolveApiKey("openai-codex"), -]); +const [openaiCodexToken] = await Promise.all([resolveApiKey("openai-codex")]); async function testAbortSignal(llm: Model, options: StreamOptionsWithExtras = {}) { const context: Context = { @@ -217,19 +214,6 @@ describe("AI Providers Abort Tests", () => { }); }); - // Google Gemini CLI / Antigravity share the same provider, so one test covers both - describe("Google Gemini CLI Provider Abort", () => { - it.skipIf(!geminiCliToken)("should abort mid-stream", { retry: 3 }, async () => { - const llm = getModel("google-gemini-cli", "gemini-2.5-flash"); - await testAbortSignal(llm, { apiKey: geminiCliToken }); - }); - - it.skipIf(!geminiCliToken)("should handle immediate abort", { retry: 3 }, async () => { - const llm = getModel("google-gemini-cli", "gemini-2.5-flash"); - await testImmediateAbort(llm, { apiKey: geminiCliToken }); - }); - }); - describe("OpenAI Codex Provider Abort", () => { it.skipIf(!openaiCodexToken)("should abort mid-stream", { retry: 3 }, async () => { const llm = getModel("openai-codex", "gpt-5.2-codex"); diff --git a/packages/ai/test/context-overflow.test.ts b/packages/ai/test/context-overflow.test.ts index aa34fd29..bd977e79 100644 --- a/packages/ai/test/context-overflow.test.ts +++ b/packages/ai/test/context-overflow.test.ts @@ -23,13 +23,8 @@ import { hasBedrockCredentials } from "./bedrock-utils.js"; import { resolveApiKey } from "./oauth.js"; // Resolve OAuth tokens at module level (async, runs before tests) -const oauthTokens = await Promise.all([ - resolveApiKey("github-copilot"), - resolveApiKey("google-gemini-cli"), - resolveApiKey("google-antigravity"), - resolveApiKey("openai-codex"), -]); -const [githubCopilotToken, geminiCliToken, antigravityToken, openaiCodexToken] = oauthTokens; +const oauthTokens = await Promise.all([resolveApiKey("github-copilot"), resolveApiKey("openai-codex")]); +const [githubCopilotToken, openaiCodexToken] = oauthTokens; // Lorem ipsum paragraph for realistic token estimation const LOREM_IPSUM = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. `; @@ -220,64 +215,12 @@ describe("Context overflow error handling", () => { }); // ============================================================================= - // Google Gemini CLI (OAuth) // Uses same API as Google, expects same error pattern // ============================================================================= - describe("Google Gemini CLI (OAuth)", () => { - it.skipIf(!geminiCliToken)( - "gemini-2.5-flash - should detect overflow via isContextOverflow", - async () => { - const model = getModel("google-gemini-cli", "gemini-2.5-flash"); - const result = await testContextOverflow(model, geminiCliToken!); - logResult(result); - - expect(result.stopReason).toBe("error"); - expect(result.errorMessage).toMatch(/input token count.*exceeds the maximum/i); - expect(isContextOverflow(result.response, model.contextWindow)).toBe(true); - }, - 120000, - ); - }); - // ============================================================================= - // Google Antigravity (OAuth) - // Tests both Gemini and Anthropic models via Antigravity // ============================================================================= - describe("Google Antigravity (OAuth)", () => { - // Gemini model - it.skipIf(!antigravityToken)( - "gemini-3-flash - should detect overflow via isContextOverflow", - async () => { - const model = getModel("google-antigravity", "gemini-3-flash"); - const result = await testContextOverflow(model, antigravityToken!); - logResult(result); - - expect(result.stopReason).toBe("error"); - expect(result.errorMessage).toMatch(/input token count.*exceeds the maximum/i); - expect(isContextOverflow(result.response, model.contextWindow)).toBe(true); - }, - 120000, - ); - - // Anthropic model via Antigravity - it.skipIf(!antigravityToken)( - "claude-sonnet-4-5 - should detect overflow via isContextOverflow", - async () => { - const model = getModel("google-antigravity", "claude-sonnet-4-5"); - const result = await testContextOverflow(model, antigravityToken!); - logResult(result); - - expect(result.stopReason).toBe("error"); - // Anthropic models return "prompt is too long" pattern - expect(result.errorMessage).toMatch(/prompt is too long/i); - expect(isContextOverflow(result.response, model.contextWindow)).toBe(true); - }, - 120000, - ); - }); - // ============================================================================= // OpenAI Codex (OAuth) // Uses ChatGPT Plus/Pro subscription via OAuth diff --git a/packages/ai/test/cross-provider-handoff.test.ts b/packages/ai/test/cross-provider-handoff.test.ts index f3c95008..2f24b01d 100644 --- a/packages/ai/test/cross-provider-handoff.test.ts +++ b/packages/ai/test/cross-provider-handoff.test.ts @@ -66,9 +66,6 @@ const PROVIDER_MODEL_PAIRS: ProviderModelPair[] = [ { provider: "azure-openai-responses", model: "gpt-4o-mini", label: "azure-openai-responses-gpt-4o-mini" }, // OpenAI Codex { provider: "openai-codex", model: "gpt-5.2-codex", label: "openai-codex-gpt-5.2-codex" }, - // Google Antigravity - { provider: "google-antigravity", model: "gemini-3-flash", label: "antigravity-gemini-3-flash" }, - { provider: "google-antigravity", model: "claude-sonnet-4-5", label: "antigravity-claude-sonnet-4-5" }, // GitHub Copilot { provider: "github-copilot", model: "claude-sonnet-4.5", label: "copilot-claude-sonnet-4.5" }, { provider: "github-copilot", model: "gpt-5.1-codex", label: "copilot-gpt-5.1-codex" }, diff --git a/packages/ai/test/empty.test.ts b/packages/ai/test/empty.test.ts index 3b2553ea..230abbb1 100644 --- a/packages/ai/test/empty.test.ts +++ b/packages/ai/test/empty.test.ts @@ -13,11 +13,9 @@ import { resolveApiKey } from "./oauth.js"; const oauthTokens = await Promise.all([ resolveApiKey("anthropic"), resolveApiKey("github-copilot"), - resolveApiKey("google-gemini-cli"), - resolveApiKey("google-antigravity"), resolveApiKey("openai-codex"), ]); -const [anthropicOAuthToken, githubCopilotToken, geminiCliToken, antigravityToken, openaiCodexToken] = oauthTokens; +const [anthropicOAuthToken, githubCopilotToken, openaiCodexToken] = oauthTokens; async function testEmptyMessage(llm: Model, options: StreamOptionsWithExtras = {}) { // Test with completely empty content array @@ -577,154 +575,6 @@ describe("AI Providers Empty Message Tests", () => { ); }); - describe("Google Gemini CLI Provider Empty Messages", () => { - it.skipIf(!geminiCliToken)( - "gemini-2.5-flash - should handle empty content array", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-gemini-cli", "gemini-2.5-flash"); - await testEmptyMessage(llm, { apiKey: geminiCliToken }); - }, - ); - - it.skipIf(!geminiCliToken)( - "gemini-2.5-flash - should handle empty string content", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-gemini-cli", "gemini-2.5-flash"); - await testEmptyStringMessage(llm, { apiKey: geminiCliToken }); - }, - ); - - it.skipIf(!geminiCliToken)( - "gemini-2.5-flash - should handle whitespace-only content", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-gemini-cli", "gemini-2.5-flash"); - await testWhitespaceOnlyMessage(llm, { apiKey: geminiCliToken }); - }, - ); - - it.skipIf(!geminiCliToken)( - "gemini-2.5-flash - should handle empty assistant message in conversation", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-gemini-cli", "gemini-2.5-flash"); - await testEmptyAssistantMessage(llm, { apiKey: geminiCliToken }); - }, - ); - }); - - describe("Google Antigravity Provider Empty Messages", () => { - it.skipIf(!antigravityToken)( - "gemini-3-flash - should handle empty content array", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-antigravity", "gemini-3-flash"); - await testEmptyMessage(llm, { apiKey: antigravityToken }); - }, - ); - - it.skipIf(!antigravityToken)( - "gemini-3-flash - should handle empty string content", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-antigravity", "gemini-3-flash"); - await testEmptyStringMessage(llm, { apiKey: antigravityToken }); - }, - ); - - it.skipIf(!antigravityToken)( - "gemini-3-flash - should handle whitespace-only content", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-antigravity", "gemini-3-flash"); - await testWhitespaceOnlyMessage(llm, { apiKey: antigravityToken }); - }, - ); - - it.skipIf(!antigravityToken)( - "gemini-3-flash - should handle empty assistant message in conversation", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-antigravity", "gemini-3-flash"); - await testEmptyAssistantMessage(llm, { apiKey: antigravityToken }); - }, - ); - - it.skipIf(!antigravityToken)( - "claude-sonnet-4-5 - should handle empty content array", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-antigravity", "claude-sonnet-4-5"); - await testEmptyMessage(llm, { apiKey: antigravityToken }); - }, - ); - - it.skipIf(!antigravityToken)( - "claude-sonnet-4-5 - should handle empty string content", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-antigravity", "claude-sonnet-4-5"); - await testEmptyStringMessage(llm, { apiKey: antigravityToken }); - }, - ); - - it.skipIf(!antigravityToken)( - "claude-sonnet-4-5 - should handle whitespace-only content", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-antigravity", "claude-sonnet-4-5"); - await testWhitespaceOnlyMessage(llm, { apiKey: antigravityToken }); - }, - ); - - it.skipIf(!antigravityToken)( - "claude-sonnet-4-5 - should handle empty assistant message in conversation", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-antigravity", "claude-sonnet-4-5"); - await testEmptyAssistantMessage(llm, { apiKey: antigravityToken }); - }, - ); - - it.skipIf(!antigravityToken)( - "gpt-oss-120b-medium - should handle empty content array", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-antigravity", "gpt-oss-120b-medium"); - await testEmptyMessage(llm, { apiKey: antigravityToken }); - }, - ); - - it.skipIf(!antigravityToken)( - "gpt-oss-120b-medium - should handle empty string content", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-antigravity", "gpt-oss-120b-medium"); - await testEmptyStringMessage(llm, { apiKey: antigravityToken }); - }, - ); - - it.skipIf(!antigravityToken)( - "gpt-oss-120b-medium - should handle whitespace-only content", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-antigravity", "gpt-oss-120b-medium"); - await testWhitespaceOnlyMessage(llm, { apiKey: antigravityToken }); - }, - ); - - it.skipIf(!antigravityToken)( - "gpt-oss-120b-medium - should handle empty assistant message in conversation", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-antigravity", "gpt-oss-120b-medium"); - await testEmptyAssistantMessage(llm, { apiKey: antigravityToken }); - }, - ); - }); - describe("OpenAI Codex Provider Empty Messages", () => { it.skipIf(!openaiCodexToken)( "gpt-5.2-codex - should handle empty content array", diff --git a/packages/ai/test/google-gemini-cli-claude-thinking-header.test.ts b/packages/ai/test/google-gemini-cli-claude-thinking-header.test.ts deleted file mode 100644 index 227becaa..00000000 --- a/packages/ai/test/google-gemini-cli-claude-thinking-header.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import { streamGoogleGeminiCli } from "../src/providers/google-gemini-cli.js"; -import type { Context, Model } from "../src/types.js"; - -const originalFetch = global.fetch; -const apiKey = JSON.stringify({ token: "token", projectId: "project" }); - -const createSseResponse = () => { - const sse = `${[ - `data: ${JSON.stringify({ - response: { - candidates: [ - { - content: { role: "model", parts: [{ text: "Hello" }] }, - finishReason: "STOP", - }, - ], - }, - })}`, - ].join("\n\n")}\n\n`; - - const encoder = new TextEncoder(); - const stream = new ReadableStream({ - start(controller) { - controller.enqueue(encoder.encode(sse)); - controller.close(); - }, - }); - - return new Response(stream, { - status: 200, - headers: { "content-type": "text/event-stream" }, - }); -}; - -afterEach(() => { - global.fetch = originalFetch; - vi.restoreAllMocks(); -}); - -describe("google-gemini-cli Claude thinking header", () => { - const context: Context = { - messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }], - }; - - it("adds anthropic-beta for Claude thinking models", async () => { - const fetchMock = vi.fn(async (_input: string | URL, init?: RequestInit) => { - const headers = new Headers(init?.headers); - expect(headers.get("anthropic-beta")).toBe("interleaved-thinking-2025-05-14"); - return createSseResponse(); - }); - - global.fetch = fetchMock as typeof fetch; - - const model: Model<"google-gemini-cli"> = { - id: "claude-opus-4-5-thinking", - name: "Claude Opus 4.5 Thinking", - api: "google-gemini-cli", - provider: "google-antigravity", - baseUrl: "https://cloudcode-pa.googleapis.com", - reasoning: true, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 128000, - maxTokens: 8192, - }; - - const stream = streamGoogleGeminiCli(model, context, { apiKey }); - for await (const _event of stream) { - // exhaust stream - } - await stream.result(); - }); - - it("does not add anthropic-beta for Gemini models", async () => { - const fetchMock = vi.fn(async (_input: string | URL, init?: RequestInit) => { - const headers = new Headers(init?.headers); - expect(headers.has("anthropic-beta")).toBe(false); - return createSseResponse(); - }); - - global.fetch = fetchMock as typeof fetch; - - const model: Model<"google-gemini-cli"> = { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash", - api: "google-gemini-cli", - provider: "google-gemini-cli", - baseUrl: "https://cloudcode-pa.googleapis.com", - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 128000, - maxTokens: 8192, - }; - - const stream = streamGoogleGeminiCli(model, context, { apiKey }); - for await (const _event of stream) { - // exhaust stream - } - await stream.result(); - }); -}); diff --git a/packages/ai/test/google-gemini-cli-empty-stream.test.ts b/packages/ai/test/google-gemini-cli-empty-stream.test.ts deleted file mode 100644 index 785abe6f..00000000 --- a/packages/ai/test/google-gemini-cli-empty-stream.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import { streamGoogleGeminiCli } from "../src/providers/google-gemini-cli.js"; -import type { Context, Model } from "../src/types.js"; - -const originalFetch = global.fetch; - -afterEach(() => { - global.fetch = originalFetch; - vi.restoreAllMocks(); -}); - -describe("google-gemini-cli empty stream retry", () => { - it("retries empty SSE responses without duplicate start", async () => { - const emptyStream = new ReadableStream({ - start(controller) { - controller.close(); - }, - }); - - const sse = `${[ - `data: ${JSON.stringify({ - response: { - candidates: [ - { - content: { role: "model", parts: [{ text: "Hello" }] }, - finishReason: "STOP", - }, - ], - usageMetadata: { - promptTokenCount: 1, - candidatesTokenCount: 1, - totalTokenCount: 2, - }, - }, - })}`, - ].join("\n\n")}\n\n`; - - const encoder = new TextEncoder(); - const dataStream = new ReadableStream({ - start(controller) { - controller.enqueue(encoder.encode(sse)); - controller.close(); - }, - }); - - let callCount = 0; - const fetchMock = vi.fn(async () => { - callCount += 1; - if (callCount === 1) { - return new Response(emptyStream, { - status: 200, - headers: { "content-type": "text/event-stream" }, - }); - } - return new Response(dataStream, { - status: 200, - headers: { "content-type": "text/event-stream" }, - }); - }); - - global.fetch = fetchMock as typeof fetch; - - const model: Model<"google-gemini-cli"> = { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash", - api: "google-gemini-cli", - provider: "google-gemini-cli", - baseUrl: "https://cloudcode-pa.googleapis.com", - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 128000, - maxTokens: 8192, - }; - - const context: Context = { - messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }], - }; - - const stream = streamGoogleGeminiCli(model, context, { - apiKey: JSON.stringify({ token: "token", projectId: "project" }), - }); - - let startCount = 0; - let doneCount = 0; - let text = ""; - - for await (const event of stream) { - if (event.type === "start") { - startCount += 1; - } - if (event.type === "done") { - doneCount += 1; - } - if (event.type === "text_delta") { - text += event.delta; - } - } - - const result = await stream.result(); - - expect(text).toBe("Hello"); - expect(result.stopReason).toBe("stop"); - expect(startCount).toBe(1); - expect(doneCount).toBe(1); - expect(fetchMock).toHaveBeenCalledTimes(2); - }); -}); diff --git a/packages/ai/test/google-gemini-cli-retry-delay.test.ts b/packages/ai/test/google-gemini-cli-retry-delay.test.ts deleted file mode 100644 index cea40940..00000000 --- a/packages/ai/test/google-gemini-cli-retry-delay.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import { extractRetryDelay } from "../src/providers/google-gemini-cli.js"; - -describe("extractRetryDelay header parsing", () => { - afterEach(() => { - vi.useRealTimers(); - }); - - it("prefers Retry-After seconds header", () => { - vi.useFakeTimers(); - vi.setSystemTime(new Date("2025-01-01T00:00:00Z")); - - const response = new Response("", { headers: { "Retry-After": "5" } }); - const delay = extractRetryDelay("Please retry in 1s", response); - - expect(delay).toBe(6000); - }); - - it("parses Retry-After HTTP date header", () => { - vi.useFakeTimers(); - const now = new Date("2025-01-01T00:00:00Z"); - vi.setSystemTime(now); - - const retryAt = new Date(now.getTime() + 12000).toUTCString(); - const response = new Response("", { headers: { "Retry-After": retryAt } }); - const delay = extractRetryDelay("", response); - - expect(delay).toBe(13000); - }); - - it("parses x-ratelimit-reset header", () => { - vi.useFakeTimers(); - const now = new Date("2025-01-01T00:00:00Z"); - vi.setSystemTime(now); - - const resetAtMs = now.getTime() + 20000; - const resetSeconds = Math.floor(resetAtMs / 1000).toString(); - const response = new Response("", { headers: { "x-ratelimit-reset": resetSeconds } }); - const delay = extractRetryDelay("", response); - - expect(delay).toBe(21000); - }); - - it("parses x-ratelimit-reset-after header", () => { - vi.useFakeTimers(); - vi.setSystemTime(new Date("2025-01-01T00:00:00Z")); - - const response = new Response("", { headers: { "x-ratelimit-reset-after": "30" } }); - const delay = extractRetryDelay("", response); - - expect(delay).toBe(31000); - }); -}); diff --git a/packages/ai/test/google-shared-gemini3-unsigned-tool-call.test.ts b/packages/ai/test/google-shared-gemini3-unsigned-tool-call.test.ts index 5e51b876..b1d09fb4 100644 --- a/packages/ai/test/google-shared-gemini3-unsigned-tool-call.test.ts +++ b/packages/ai/test/google-shared-gemini3-unsigned-tool-call.test.ts @@ -34,11 +34,11 @@ describe("google-shared convertMessages — Gemini 3 unsigned tool calls", () => id: "call_1", name: "bash", arguments: { command: "ls -la" }, - // No thoughtSignature: simulates Claude via Antigravity. + // No thoughtSignature: simulates replay from a provider without thought signatures. }, ], - api: "google-gemini-cli", - provider: "google-antigravity", + api: "google-generative-ai", + provider: "google", model: "claude-sonnet-4-6", usage: { input: 0, @@ -144,8 +144,8 @@ describe("google-shared convertMessages — Gemini 3 unsigned tool calls", () => // No thoughtSignature }, ], - api: "google-gemini-cli", - provider: "google-antigravity", + api: "google-generative-ai", + provider: "google", model: "claude-sonnet-4-6", usage: { input: 0, diff --git a/packages/ai/test/google-shared-image-tool-result-routing.test.ts b/packages/ai/test/google-shared-image-tool-result-routing.test.ts index c3a991d5..27150dba 100644 --- a/packages/ai/test/google-shared-image-tool-result-routing.test.ts +++ b/packages/ai/test/google-shared-image-tool-result-routing.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { convertMessages } from "../src/providers/google-shared.js"; import type { Context, Model } from "../src/types.js"; -function makeModel( +function makeModel( api: TApi, provider: Model["provider"], id: string, @@ -99,26 +99,4 @@ describe("google-shared image tool result routing", () => { expect(imageResponse?.parts).toHaveLength(1); expect(imageResponse?.parts?.[0]?.inlineData).toBeTruthy(); }); - - it("nests image tool results for non-Gemini models on Antigravity / Cloud Code Assist", () => { - const model = makeModel("google-gemini-cli", "google-antigravity", "claude-sonnet-4-6"); - const contents = convertMessages(model, makeContext(model)); - - expect(contents).toHaveLength(3); - const toolResultTurn = contents[2]; - expect(toolResultTurn.parts).toHaveLength(3); - const imageResponse = toolResultTurn.parts?.[1]?.functionResponse; - expect(imageResponse).toBeTruthy(); - expect(imageResponse?.parts).toHaveLength(1); - expect(imageResponse?.parts?.[0]?.inlineData).toBeTruthy(); - }); - - it("keeps separate synthetic image turn for Gemini 2.x Cloud Code Assist models", () => { - const model = makeModel("google-gemini-cli", "google-gemini-cli", "gemini-2.5-flash"); - const contents = convertMessages(model, makeContext(model)); - - expect(contents).toHaveLength(5); - expect(contents[3].parts?.[0]?.text).toBe("Tool result image:"); - expect(contents[3].parts?.[1]?.inlineData).toBeTruthy(); - }); }); diff --git a/packages/ai/test/google-thinking-disable.test.ts b/packages/ai/test/google-thinking-disable.test.ts index 6d38ad71..e8720ff8 100644 --- a/packages/ai/test/google-thinking-disable.test.ts +++ b/packages/ai/test/google-thinking-disable.test.ts @@ -2,7 +2,6 @@ import { describe, expect, it } from "vitest"; import { getModel } from "../src/models.js"; import { streamSimple } from "../src/stream.js"; import type { Api, Context, Model, SimpleStreamOptions } from "../src/types.js"; -import { resolveApiKey } from "./oauth.js"; type SimpleOptionsWithExtras = SimpleStreamOptions & Record; @@ -20,9 +19,6 @@ interface DisableExpectations { maxOutputTokens?: number; } -const oauthTokens = await Promise.all([resolveApiKey("google-gemini-cli"), resolveApiKey("google-antigravity")]); -const [geminiCliToken, antigravityToken] = oauthTokens; - function makeContext(): Context { return { systemPrompt: "You are a precise assistant. Follow the requested output format exactly.", @@ -148,24 +144,6 @@ describe("Google Vertex thinking disable E2E", () => { }); }); -describe("Google Gemini CLI thinking disable E2E", () => { - it.skipIf(!geminiCliToken)("disables thinking for Gemini 2.5", { retry: 2, timeout: 30000 }, async () => { - await expectThinkingDisabledE2E(getModel("google-gemini-cli", "gemini-2.5-flash"), { - requestOptions: { apiKey: geminiCliToken! }, - maxOutputTokens: 100, - }); - }); -}); - -describe("Google Antigravity thinking disable E2E", () => { - it.skipIf(!antigravityToken)("disables thinking for Gemini 3.x", { retry: 2, timeout: 30000 }, async () => { - await expectThinkingDisabledE2E(getModel("google-antigravity", "gemini-3-flash"), { - requestOptions: { apiKey: antigravityToken! }, - maxOutputTokens: 100, - }); - }); -}); - describe.skipIf(!process.env.OPENAI_API_KEY)("OpenAI thinking disable E2E", () => { it("disables thinking for Responses reasoning models", { retry: 2, timeout: 30000 }, async () => { await expectThinkingDisabledE2E(getModel("openai", "gpt-5.4-mini"), { diff --git a/packages/ai/test/google-tool-call-missing-args.test.ts b/packages/ai/test/google-tool-call-missing-args.test.ts deleted file mode 100644 index 2082502f..00000000 --- a/packages/ai/test/google-tool-call-missing-args.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { Type } from "typebox"; -import { afterEach, describe, expect, it, vi } from "vitest"; -import { streamGoogleGeminiCli } from "../src/providers/google-gemini-cli.js"; -import type { Context, Model, ToolCall } from "../src/types.js"; - -const emptySchema = Type.Object({}); - -const originalFetch = global.fetch; - -afterEach(() => { - global.fetch = originalFetch; - vi.restoreAllMocks(); -}); - -describe("google providers tool call missing args", () => { - it("defaults arguments to empty object when provider omits args field", async () => { - // Simulate a tool call response where args is missing (no-arg tool) - const sse = `${[ - `data: ${JSON.stringify({ - response: { - candidates: [ - { - content: { - role: "model", - parts: [ - { - functionCall: { - name: "get_status", - // args intentionally omitted - }, - }, - ], - }, - finishReason: "STOP", - }, - ], - usageMetadata: { - promptTokenCount: 10, - candidatesTokenCount: 5, - totalTokenCount: 15, - }, - }, - })}`, - ].join("\n\n")}\n\n`; - - const encoder = new TextEncoder(); - const dataStream = new ReadableStream({ - start(controller) { - controller.enqueue(encoder.encode(sse)); - controller.close(); - }, - }); - - const fetchMock = vi.fn(async () => { - return new Response(dataStream, { - status: 200, - headers: { "content-type": "text/event-stream" }, - }); - }); - - global.fetch = fetchMock as typeof fetch; - - const model: Model<"google-gemini-cli"> = { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash", - api: "google-gemini-cli", - provider: "google-gemini-cli", - baseUrl: "https://cloudcode-pa.googleapis.com", - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 128000, - maxTokens: 8192, - }; - - const context: Context = { - messages: [{ role: "user", content: "Check status", timestamp: Date.now() }], - tools: [ - { - name: "get_status", - description: "Get current status", - parameters: emptySchema, - }, - ], - }; - - const stream = streamGoogleGeminiCli(model, context, { - apiKey: JSON.stringify({ token: "token", projectId: "project" }), - }); - - for await (const _ of stream) { - // consume stream - } - - const result = await stream.result(); - - expect(result.stopReason).toBe("toolUse"); - expect(result.content).toHaveLength(1); - - const toolCall = result.content[0] as ToolCall; - expect(toolCall.type).toBe("toolCall"); - expect(toolCall.name).toBe("get_status"); - expect(toolCall.arguments).toEqual({}); - }); -}); diff --git a/packages/ai/test/image-tool-result.test.ts b/packages/ai/test/image-tool-result.test.ts index 3b02dd55..74910c35 100644 --- a/packages/ai/test/image-tool-result.test.ts +++ b/packages/ai/test/image-tool-result.test.ts @@ -16,11 +16,9 @@ import { resolveApiKey } from "./oauth.js"; const oauthTokens = await Promise.all([ resolveApiKey("anthropic"), resolveApiKey("github-copilot"), - resolveApiKey("google-gemini-cli"), - resolveApiKey("google-antigravity"), resolveApiKey("openai-codex"), ]); -const [anthropicOAuthToken, githubCopilotToken, geminiCliToken, antigravityToken, openaiCodexToken] = oauthTokens; +const [anthropicOAuthToken, githubCopilotToken, openaiCodexToken] = oauthTokens; /** * Test that tool results containing only images work correctly across all providers. @@ -398,67 +396,6 @@ describe("Tool Results with Images", () => { ); }); - describe("Google Gemini CLI Provider", () => { - it.skipIf(!geminiCliToken)( - "gemini-2.5-flash - should handle tool result with only image", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-gemini-cli", "gemini-2.5-flash"); - await handleToolWithImageResult(llm, { apiKey: geminiCliToken }); - }, - ); - - it.skipIf(!geminiCliToken)( - "gemini-2.5-flash - should handle tool result with text and image", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-gemini-cli", "gemini-2.5-flash"); - await handleToolWithTextAndImageResult(llm, { apiKey: geminiCliToken }); - }, - ); - }); - - describe("Google Antigravity Provider", () => { - it.skipIf(!antigravityToken)( - "gemini-3-flash - should handle tool result with only image", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-antigravity", "gemini-3-flash"); - await handleToolWithImageResult(llm, { apiKey: antigravityToken }); - }, - ); - - it.skipIf(!antigravityToken)( - "gemini-3-flash - should handle tool result with text and image", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-antigravity", "gemini-3-flash"); - await handleToolWithTextAndImageResult(llm, { apiKey: antigravityToken }); - }, - ); - - /** These two don't work, the model simply won't call the tool, works in pi - it.skipIf(!antigravityToken)( - "claude-sonnet-4-5 - should handle tool result with only image", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-antigravity", "claude-sonnet-4-5"); - await handleToolWithImageResult(llm, { apiKey: antigravityToken }); - }, - ); - - it.skipIf(!antigravityToken)( - "claude-sonnet-4-5 - should handle tool result with text and image", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-antigravity", "claude-sonnet-4-5"); - await handleToolWithTextAndImageResult(llm, { apiKey: antigravityToken }); - }, - );**/ - - // Note: gpt-oss-120b-medium does not support images, so not tested here - }); - describe("OpenAI Codex Provider", () => { it.skipIf(!openaiCodexToken)( "gpt-5.2-codex - should handle tool result with only image", diff --git a/packages/ai/test/oauth.ts b/packages/ai/test/oauth.ts index b026906c..bd32f3d6 100644 --- a/packages/ai/test/oauth.ts +++ b/packages/ai/test/oauth.ts @@ -53,7 +53,6 @@ function saveAuthStorage(storage: AuthStorage): void { * For API key credentials, returns the key directly. * For OAuth credentials, returns the access token (refreshing if expired and saving back). * - * For google-gemini-cli and google-antigravity, returns JSON-encoded { token, projectId } */ export async function resolveApiKey(provider: string): Promise { const storage = loadAuthStorage(); diff --git a/packages/ai/test/responseid.test.ts b/packages/ai/test/responseid.test.ts index d03ce5bd..6181ffa3 100644 --- a/packages/ai/test/responseid.test.ts +++ b/packages/ai/test/responseid.test.ts @@ -7,13 +7,8 @@ import { resolveApiKey } from "./oauth.js"; type StreamOptionsWithExtras = StreamOptions & Record; -const oauthTokens = await Promise.all([ - resolveApiKey("github-copilot"), - resolveApiKey("google-gemini-cli"), - resolveApiKey("google-antigravity"), - resolveApiKey("openai-codex"), -]); -const [githubCopilotToken, geminiCliToken, antigravityToken, openaiCodexToken] = oauthTokens; +const oauthTokens = await Promise.all([resolveApiKey("github-copilot"), resolveApiKey("openai-codex")]); +const [githubCopilotToken, openaiCodexToken] = oauthTokens; async function expectResponseId(model: Model, options: StreamOptionsWithExtras = {}) { const context: Context = { @@ -117,25 +112,6 @@ describe("responseId E2E Tests", () => { ); }); - describe("Google Gemini CLI Provider", () => { - it.skipIf(!geminiCliToken)("should expose responseId", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("google-gemini-cli", "gemini-2.5-flash"); - await expectResponseId(llm, { apiKey: geminiCliToken }); - }); - }); - - describe("Google Antigravity Provider", () => { - it.skipIf(!antigravityToken)("Gemini path should expose responseId", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("google-antigravity", "gemini-3.1-pro-high"); - await expectResponseId(llm, { apiKey: antigravityToken }); - }); - - it.skipIf(!antigravityToken)("Claude path should expose responseId", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("google-antigravity", "claude-sonnet-4-6"); - await expectResponseId(llm, { apiKey: antigravityToken }); - }); - }); - describe("OpenAI Codex Provider", () => { it.skipIf(!openaiCodexToken)("should expose responseId", { retry: 3, timeout: 30000 }, async () => { const llm = getModel("openai-codex", "gpt-5.2-codex"); diff --git a/packages/ai/test/stream.test.ts b/packages/ai/test/stream.test.ts index 687a094f..41816b7e 100644 --- a/packages/ai/test/stream.test.ts +++ b/packages/ai/test/stream.test.ts @@ -22,11 +22,9 @@ const __dirname = dirname(__filename); const oauthTokens = await Promise.all([ resolveApiKey("anthropic"), resolveApiKey("github-copilot"), - resolveApiKey("google-gemini-cli"), - resolveApiKey("google-antigravity"), resolveApiKey("openai-codex"), ]); -const [anthropicOAuthToken, githubCopilotToken, geminiCliToken, antigravityToken, openaiCodexToken] = oauthTokens; +const [anthropicOAuthToken, githubCopilotToken, openaiCodexToken] = oauthTokens; // Calculator tool definition (same as examples) // Note: Using StringEnum helper because Google's API doesn't support anyOf/const patterns @@ -1028,124 +1026,6 @@ describe("Generate E2E Tests", () => { }); }); - describe("Google Gemini CLI Provider (gemini-2.5-flash)", () => { - const llm = getModel("google-gemini-cli", "gemini-2.5-flash"); - - it.skipIf(!geminiCliToken)("should complete basic text generation", { retry: 3 }, async () => { - await basicTextGeneration(llm, { apiKey: geminiCliToken }); - }); - - it.skipIf(!geminiCliToken)("should handle tool calling", { retry: 3 }, async () => { - await handleToolCall(llm, { apiKey: geminiCliToken }); - }); - - it.skipIf(!geminiCliToken)("should handle streaming", { retry: 3 }, async () => { - await handleStreaming(llm, { apiKey: geminiCliToken }); - }); - - it.skipIf(!geminiCliToken)("should handle thinking", { retry: 3 }, async () => { - await handleThinking(llm, { apiKey: geminiCliToken, thinking: { enabled: true, budgetTokens: 1024 } }); - }); - - it.skipIf(!geminiCliToken)("should handle multi-turn with thinking and tools", { retry: 3 }, async () => { - await multiTurn(llm, { apiKey: geminiCliToken, thinking: { enabled: true, budgetTokens: 2048 } }); - }); - - it.skipIf(!geminiCliToken)("should handle image input", { retry: 3 }, async () => { - await handleImage(llm, { apiKey: geminiCliToken }); - }); - }); - - describe("Google Gemini CLI Provider (gemini-3-flash-preview with thinkingLevel)", () => { - const llm = getModel("google-gemini-cli", "gemini-3-flash-preview"); - - it.skipIf(!geminiCliToken)("should handle thinking with thinkingLevel", { retry: 3 }, async () => { - await handleThinking(llm, { apiKey: geminiCliToken, thinking: { enabled: true, level: "LOW" } }); - }); - - it.skipIf(!geminiCliToken)("should handle multi-turn with thinking and tools", { retry: 3 }, async () => { - await multiTurn(llm, { apiKey: geminiCliToken, thinking: { enabled: true, level: "MEDIUM" } }); - }); - }); - - describe("Google Antigravity Provider (gemini-3.1-pro-high)", () => { - const llm = getModel("google-antigravity", "gemini-3.1-pro-high"); - - it.skipIf(!antigravityToken)("should complete basic text generation", { retry: 3 }, async () => { - await basicTextGeneration(llm, { apiKey: antigravityToken }); - }); - - it.skipIf(!antigravityToken)("should handle tool calling", { retry: 3 }, async () => { - await handleToolCall(llm, { apiKey: antigravityToken }); - }); - - it.skipIf(!antigravityToken)("should handle streaming", { retry: 3 }, async () => { - await handleStreaming(llm, { apiKey: antigravityToken }); - }); - - it.skipIf(!antigravityToken)("should handle thinking with thinkingLevel", { retry: 3 }, async () => { - // gemini-3-pro only supports LOW/HIGH - await handleThinking(llm, { - apiKey: antigravityToken, - thinking: { enabled: true, level: "LOW" }, - }); - }); - - it.skipIf(!antigravityToken)("should handle multi-turn with thinking and tools", { retry: 3 }, async () => { - await multiTurn(llm, { apiKey: antigravityToken, thinking: { enabled: true, level: "HIGH" } }); - }); - - it.skipIf(!antigravityToken)("should handle image input", { retry: 3 }, async () => { - await handleImage(llm, { apiKey: antigravityToken }); - }); - }); - - describe("Google Antigravity Provider (gemini-3.1-pro-high with thinkingLevel)", () => { - const llm = getModel("google-antigravity", "gemini-3.1-pro-high"); - - it.skipIf(!antigravityToken)("should handle thinking with thinkingLevel HIGH", { retry: 3 }, async () => { - // gemini-3-pro only supports LOW/HIGH - await handleThinking(llm, { - apiKey: antigravityToken, - thinking: { enabled: true, level: "HIGH" }, - }); - }); - }); - - describe("Google Antigravity Provider (claude-sonnet-4-5)", () => { - const llm = getModel("google-antigravity", "claude-sonnet-4-5"); - - it.skipIf(!antigravityToken)("should complete basic text generation", { retry: 3 }, async () => { - await basicTextGeneration(llm, { apiKey: antigravityToken }); - }); - - it.skipIf(!antigravityToken)("should handle tool calling", { retry: 3 }, async () => { - await handleToolCall(llm, { apiKey: antigravityToken }); - }); - - it.skipIf(!antigravityToken)("should handle streaming", { retry: 3 }, async () => { - await handleStreaming(llm, { apiKey: antigravityToken }); - }); - - it.skipIf(!antigravityToken)("should handle thinking", { retry: 3 }, async () => { - // claude-sonnet-4-5 has reasoning: false, use claude-sonnet-4-5-thinking - const thinkingModel = getModel("google-antigravity", "claude-sonnet-4-5-thinking"); - await handleThinking(thinkingModel, { - apiKey: antigravityToken, - thinking: { enabled: true, budgetTokens: 4096 }, - }); - }); - - it.skipIf(!antigravityToken)("should handle multi-turn with thinking and tools", { retry: 3 }, async () => { - const thinkingModel = getModel("google-antigravity", "claude-sonnet-4-5-thinking"); - await multiTurn(thinkingModel, { apiKey: antigravityToken, thinking: { enabled: true, budgetTokens: 4096 } }); - }); - - it.skipIf(!antigravityToken)("should handle image input", { retry: 3 }, async () => { - await handleImage(llm, { apiKey: antigravityToken }); - }); - }); - describe("OpenAI Codex Provider (gpt-5.4)", () => { const llm = getModel("openai-codex", "gpt-5.4"); diff --git a/packages/ai/test/tokens.test.ts b/packages/ai/test/tokens.test.ts index a128ec0d..bd1ee8c0 100644 --- a/packages/ai/test/tokens.test.ts +++ b/packages/ai/test/tokens.test.ts @@ -13,11 +13,9 @@ import { resolveApiKey } from "./oauth.js"; const oauthTokens = await Promise.all([ resolveApiKey("anthropic"), resolveApiKey("github-copilot"), - resolveApiKey("google-gemini-cli"), - resolveApiKey("google-antigravity"), resolveApiKey("openai-codex"), ]); -const [anthropicOAuthToken, githubCopilotToken, geminiCliToken, antigravityToken, openaiCodexToken] = oauthTokens; +const [anthropicOAuthToken, githubCopilotToken, openaiCodexToken] = oauthTokens; async function testTokensOnAbort(llm: Model, options: StreamOptionsWithExtras = {}) { const context: Context = { @@ -50,7 +48,7 @@ async function testTokensOnAbort(llm: Model, options: St expect(msg.stopReason).toBe("aborted"); - // OpenAI providers, OpenAI Codex, Gemini CLI, zai, Amazon Bedrock, and the GPT-OSS model on Antigravity only send usage in the final chunk, + // OpenAI providers, OpenAI Codex, zai, and Amazon Bedrock only send usage in the final chunk, // so when aborted they have no token stats. Anthropic and Google send usage information early in the stream. // MiniMax and Kimi report input tokens but not output tokens differently on aborted requests. if ( @@ -59,11 +57,9 @@ async function testTokensOnAbort(llm: Model, options: St llm.api === "openai-responses" || llm.api === "azure-openai-responses" || llm.api === "openai-codex-responses" || - llm.provider === "google-gemini-cli" || llm.provider === "zai" || llm.provider === "amazon-bedrock" || - llm.provider === "vercel-ai-gateway" || - (llm.provider === "google-antigravity" && llm.id.includes("gpt-oss")) + llm.provider === "vercel-ai-gateway" ) { expect(msg.usage.input).toBe(0); expect(msg.usage.output).toBe(0); @@ -79,7 +75,7 @@ async function testTokensOnAbort(llm: Model, options: St expect(msg.usage.input).toBeGreaterThan(0); expect(msg.usage.output).toBeGreaterThan(0); - // Some providers (Antigravity, Copilot) have zero cost rates + // Some providers (Copilot) have zero cost rates if (llm.cost.input > 0) { expect(msg.usage.cost.input).toBeGreaterThan(0); expect(msg.usage.cost.total).toBeGreaterThan(0); @@ -254,46 +250,6 @@ describe("Token Statistics on Abort", () => { ); }); - describe("Google Gemini CLI Provider", () => { - it.skipIf(!geminiCliToken)( - "gemini-2.5-flash - should include token stats when aborted mid-stream", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-gemini-cli", "gemini-2.5-flash"); - await testTokensOnAbort(llm, { apiKey: geminiCliToken }); - }, - ); - }); - - describe("Google Antigravity Provider", () => { - it.skipIf(!antigravityToken)( - "gemini-3-flash - should include token stats when aborted mid-stream", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-antigravity", "gemini-3-flash"); - await testTokensOnAbort(llm, { apiKey: antigravityToken }); - }, - ); - - it.skipIf(!antigravityToken)( - "claude-sonnet-4-6 - should include token stats when aborted mid-stream", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-antigravity", "claude-sonnet-4-6"); - await testTokensOnAbort(llm, { apiKey: antigravityToken }); - }, - ); - - it.skipIf(!antigravityToken)( - "gpt-oss-120b-medium - should include token stats when aborted mid-stream", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-antigravity", "gpt-oss-120b-medium"); - await testTokensOnAbort(llm, { apiKey: antigravityToken }); - }, - ); - }); - describe("OpenAI Codex Provider", () => { it.skipIf(!openaiCodexToken)( "gpt-5.2-codex - should include token stats when aborted mid-stream", diff --git a/packages/ai/test/tool-call-without-result.test.ts b/packages/ai/test/tool-call-without-result.test.ts index 9c732cd1..22822e32 100644 --- a/packages/ai/test/tool-call-without-result.test.ts +++ b/packages/ai/test/tool-call-without-result.test.ts @@ -14,11 +14,9 @@ import { resolveApiKey } from "./oauth.js"; const oauthTokens = await Promise.all([ resolveApiKey("anthropic"), resolveApiKey("github-copilot"), - resolveApiKey("google-gemini-cli"), - resolveApiKey("google-antigravity"), resolveApiKey("openai-codex"), ]); -const [anthropicOAuthToken, githubCopilotToken, geminiCliToken, antigravityToken, openaiCodexToken] = oauthTokens; +const [anthropicOAuthToken, githubCopilotToken, openaiCodexToken] = oauthTokens; // Simple calculate tool const calculateSchema = Type.Object({ @@ -275,46 +273,6 @@ describe("Tool Call Without Result Tests", () => { ); }); - describe("Google Gemini CLI Provider", () => { - it.skipIf(!geminiCliToken)( - "gemini-2.5-flash - should filter out tool calls without corresponding tool results", - { retry: 3, timeout: 30000 }, - async () => { - const model = getModel("google-gemini-cli", "gemini-2.5-flash"); - await testToolCallWithoutResult(model, { apiKey: geminiCliToken }); - }, - ); - }); - - describe("Google Antigravity Provider", () => { - it.skipIf(!antigravityToken)( - "gemini-3-flash - should filter out tool calls without corresponding tool results", - { retry: 3, timeout: 30000 }, - async () => { - const model = getModel("google-antigravity", "gemini-3-flash"); - await testToolCallWithoutResult(model, { apiKey: antigravityToken }); - }, - ); - - it.skipIf(!antigravityToken)( - "claude-sonnet-4-5 - should filter out tool calls without corresponding tool results", - { retry: 3, timeout: 30000 }, - async () => { - const model = getModel("google-antigravity", "claude-sonnet-4-5"); - await testToolCallWithoutResult(model, { apiKey: antigravityToken }); - }, - ); - - it.skipIf(!antigravityToken)( - "gpt-oss-120b-medium - should filter out tool calls without corresponding tool results", - { retry: 3, timeout: 30000 }, - async () => { - const model = getModel("google-antigravity", "gpt-oss-120b-medium"); - await testToolCallWithoutResult(model, { apiKey: antigravityToken }); - }, - ); - }); - describe("OpenAI Codex Provider", () => { it.skipIf(!openaiCodexToken)( "gpt-5.2-codex - should filter out tool calls without corresponding tool results", diff --git a/packages/ai/test/total-tokens.test.ts b/packages/ai/test/total-tokens.test.ts index 9d7235d5..300b0b0f 100644 --- a/packages/ai/test/total-tokens.test.ts +++ b/packages/ai/test/total-tokens.test.ts @@ -27,11 +27,9 @@ import { resolveApiKey } from "./oauth.js"; const oauthTokens = await Promise.all([ resolveApiKey("anthropic"), resolveApiKey("github-copilot"), - resolveApiKey("google-gemini-cli"), - resolveApiKey("google-antigravity"), resolveApiKey("openai-codex"), ]); -const [anthropicOAuthToken, githubCopilotToken, geminiCliToken, antigravityToken, openaiCodexToken] = oauthTokens; +const [anthropicOAuthToken, githubCopilotToken, openaiCodexToken] = oauthTokens; // Generate a long system prompt to trigger caching (>2k bytes for most providers) const LONG_SYSTEM_PROMPT = `You are a helpful assistant. Be concise in your responses. @@ -600,85 +598,11 @@ describe("totalTokens field", () => { }); // ========================================================================= - // Google Gemini CLI (OAuth) // ========================================================================= - describe("Google Gemini CLI (OAuth)", () => { - it.skipIf(!geminiCliToken)( - "gemini-2.5-flash - should return totalTokens equal to sum of components", - { retry: 3, timeout: 60000 }, - async () => { - const llm = getModel("google-gemini-cli", "gemini-2.5-flash"); - - console.log(`\nGoogle Gemini CLI / ${llm.id}:`); - const { first, second } = await testTotalTokensWithCache(llm, { apiKey: geminiCliToken }); - - logUsage("First request", first); - logUsage("Second request", second); - - assertTotalTokensEqualsComponents(first); - assertTotalTokensEqualsComponents(second); - }, - ); - }); - // ========================================================================= - // Google Antigravity (OAuth) // ========================================================================= - describe("Google Antigravity (OAuth)", () => { - it.skipIf(!antigravityToken)( - "gemini-3-flash - should return totalTokens equal to sum of components", - { retry: 3, timeout: 60000 }, - async () => { - const llm = getModel("google-antigravity", "gemini-3-flash"); - - console.log(`\nGoogle Antigravity / ${llm.id}:`); - const { first, second } = await testTotalTokensWithCache(llm, { apiKey: antigravityToken }); - - logUsage("First request", first); - logUsage("Second request", second); - - assertTotalTokensEqualsComponents(first); - assertTotalTokensEqualsComponents(second); - }, - ); - - it.skipIf(!antigravityToken)( - "claude-sonnet-4-5 - should return totalTokens equal to sum of components", - { retry: 3, timeout: 60000 }, - async () => { - const llm = getModel("google-antigravity", "claude-sonnet-4-5"); - - console.log(`\nGoogle Antigravity / ${llm.id}:`); - const { first, second } = await testTotalTokensWithCache(llm, { apiKey: antigravityToken }); - - logUsage("First request", first); - logUsage("Second request", second); - - assertTotalTokensEqualsComponents(first); - assertTotalTokensEqualsComponents(second); - }, - ); - - it.skipIf(!antigravityToken)( - "gpt-oss-120b-medium - should return totalTokens equal to sum of components", - { retry: 3, timeout: 60000 }, - async () => { - const llm = getModel("google-antigravity", "gpt-oss-120b-medium"); - - console.log(`\nGoogle Antigravity / ${llm.id}:`); - const { first, second } = await testTotalTokensWithCache(llm, { apiKey: antigravityToken }); - - logUsage("First request", first); - logUsage("Second request", second); - - assertTotalTokensEqualsComponents(first); - assertTotalTokensEqualsComponents(second); - }, - ); - }); - describe.skipIf(!hasBedrockCredentials())("Amazon Bedrock", () => { it( "claude-sonnet-4-5 - should return totalTokens equal to sum of components", diff --git a/packages/ai/test/unicode-surrogate.test.ts b/packages/ai/test/unicode-surrogate.test.ts index 7172624a..b32005f5 100644 --- a/packages/ai/test/unicode-surrogate.test.ts +++ b/packages/ai/test/unicode-surrogate.test.ts @@ -17,11 +17,9 @@ const emptySchema = Type.Object({}); const oauthTokens = await Promise.all([ resolveApiKey("anthropic"), resolveApiKey("github-copilot"), - resolveApiKey("google-gemini-cli"), - resolveApiKey("google-antigravity"), resolveApiKey("openai-codex"), ]); -const [anthropicOAuthToken, githubCopilotToken, geminiCliToken, antigravityToken, openaiCodexToken] = oauthTokens; +const [anthropicOAuthToken, githubCopilotToken, openaiCodexToken] = oauthTokens; /** * Test for Unicode surrogate pair handling in tool results. @@ -451,118 +449,6 @@ describe("AI Providers Unicode Surrogate Pair Tests", () => { ); }); - describe("Google Gemini CLI Provider Unicode Handling", () => { - it.skipIf(!geminiCliToken)( - "gemini-2.5-flash - should handle emoji in tool results", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-gemini-cli", "gemini-2.5-flash"); - await testEmojiInToolResults(llm, { apiKey: geminiCliToken }); - }, - ); - - it.skipIf(!geminiCliToken)( - "gemini-2.5-flash - should handle real-world LinkedIn comment data with emoji", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-gemini-cli", "gemini-2.5-flash"); - await testRealWorldLinkedInData(llm, { apiKey: geminiCliToken }); - }, - ); - - it.skipIf(!geminiCliToken)( - "gemini-2.5-flash - should handle unpaired high surrogate (0xD83D) in tool results", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-gemini-cli", "gemini-2.5-flash"); - await testUnpairedHighSurrogate(llm, { apiKey: geminiCliToken }); - }, - ); - }); - - describe("Google Antigravity Provider Unicode Handling", () => { - it.skipIf(!antigravityToken)( - "gemini-3-flash - should handle emoji in tool results", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-antigravity", "gemini-3-flash"); - await testEmojiInToolResults(llm, { apiKey: antigravityToken }); - }, - ); - - it.skipIf(!antigravityToken)( - "gemini-3-flash - should handle real-world LinkedIn comment data with emoji", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-antigravity", "gemini-3-flash"); - await testRealWorldLinkedInData(llm, { apiKey: antigravityToken }); - }, - ); - - it.skipIf(!antigravityToken)( - "gemini-3-flash - should handle unpaired high surrogate (0xD83D) in tool results", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-antigravity", "gemini-3-flash"); - await testUnpairedHighSurrogate(llm, { apiKey: antigravityToken }); - }, - ); - - it.skipIf(!antigravityToken)( - "claude-sonnet-4-5 - should handle emoji in tool results", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-antigravity", "claude-sonnet-4-5"); - await testEmojiInToolResults(llm, { apiKey: antigravityToken }); - }, - ); - - it.skipIf(!antigravityToken)( - "claude-sonnet-4-5 - should handle real-world LinkedIn comment data with emoji", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-antigravity", "claude-sonnet-4-5"); - await testRealWorldLinkedInData(llm, { apiKey: antigravityToken }); - }, - ); - - it.skipIf(!antigravityToken)( - "claude-sonnet-4-5 - should handle unpaired high surrogate (0xD83D) in tool results", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-antigravity", "claude-sonnet-4-5"); - await testUnpairedHighSurrogate(llm, { apiKey: antigravityToken }); - }, - ); - - it.skipIf(!antigravityToken)( - "gpt-oss-120b-medium - should handle emoji in tool results", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-antigravity", "gpt-oss-120b-medium"); - await testEmojiInToolResults(llm, { apiKey: antigravityToken }); - }, - ); - - it.skipIf(!antigravityToken)( - "gpt-oss-120b-medium - should handle real-world LinkedIn comment data with emoji", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-antigravity", "gpt-oss-120b-medium"); - await testRealWorldLinkedInData(llm, { apiKey: antigravityToken }); - }, - ); - - it.skipIf(!antigravityToken)( - "gpt-oss-120b-medium - should handle unpaired high surrogate (0xD83D) in tool results", - { retry: 3, timeout: 30000 }, - async () => { - const llm = getModel("google-antigravity", "gpt-oss-120b-medium"); - await testUnpairedHighSurrogate(llm, { apiKey: antigravityToken }); - }, - ); - }); - describe.skipIf(!process.env.XAI_API_KEY)("xAI Provider Unicode Handling", () => { const llm = getModel("xai", "grok-3"); diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index c034bb88..f2ad05a9 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -104,8 +104,6 @@ For each built-in provider, pi maintains a list of tool-capable models, updated - Anthropic Claude Pro/Max - OpenAI ChatGPT Plus/Pro (Codex) - GitHub Copilot -- Google Gemini CLI -- Google Antigravity **API keys:** - Anthropic diff --git a/packages/coding-agent/docs/custom-provider.md b/packages/coding-agent/docs/custom-provider.md index 21162f91..a1e68682 100644 --- a/packages/coding-agent/docs/custom-provider.md +++ b/packages/coding-agent/docs/custom-provider.md @@ -199,7 +199,6 @@ The `api` field determines which streaming implementation is used: | `openai-codex-responses` | OpenAI Codex Responses API | | `mistral-conversations` | Mistral SDK Conversations/Chat streaming | | `google-generative-ai` | Google Generative AI API | -| `google-gemini-cli` | Google Cloud Code Assist API | | `google-vertex` | Google Vertex AI API | | `bedrock-converse-stream` | Amazon Bedrock Converse API | diff --git a/packages/coding-agent/docs/providers.md b/packages/coding-agent/docs/providers.md index 436d04e2..c76834eb 100644 --- a/packages/coding-agent/docs/providers.md +++ b/packages/coding-agent/docs/providers.md @@ -18,8 +18,6 @@ Use `/login` in interactive mode, then select a provider: - Claude Pro/Max - ChatGPT Plus/Pro (Codex) - GitHub Copilot -- Google Gemini CLI -- Google Antigravity Use `/logout` to clear credentials. Tokens are stored in `~/.pi/agent/auth.json` and auto-refresh when expired. @@ -30,8 +28,6 @@ Use `/logout` to clear credentials. Tokens are stored in `~/.pi/agent/auth.json` ### Google Providers -- **Gemini CLI**: Standard Gemini models via Cloud Code Assist -- **Antigravity**: Sandbox with Gemini 3, Claude, and GPT-OSS models - Both free with any Google account, subject to rate limits - For paid Cloud Code Assist: set `GOOGLE_CLOUD_PROJECT` env var diff --git a/packages/coding-agent/docs/quickstart.md b/packages/coding-agent/docs/quickstart.md index 8d1499fe..5ae2afac 100644 --- a/packages/coding-agent/docs/quickstart.md +++ b/packages/coding-agent/docs/quickstart.md @@ -29,7 +29,7 @@ Start pi and run: /login ``` -Then select a provider. Built-in subscription logins include Claude Pro/Max, ChatGPT Plus/Pro (Codex), GitHub Copilot, Google Gemini CLI, and Google Antigravity. +Then select a provider. Built-in subscription logins include Claude Pro/Max, ChatGPT Plus/Pro (Codex), and GitHub Copilot. ### Option 2: API key diff --git a/packages/coding-agent/examples/extensions/README.md b/packages/coding-agent/examples/extensions/README.md index d62f7d74..48937dfd 100644 --- a/packages/coding-agent/examples/extensions/README.md +++ b/packages/coding-agent/examples/extensions/README.md @@ -38,7 +38,6 @@ cp permission-gate.ts ~/.pi/agent/extensions/ | `built-in-tool-renderer.ts` | Custom compact rendering for built-in tools (read, bash, edit, write) while keeping original behavior | | `minimal-mode.ts` | Override built-in tool rendering for minimal display (only tool calls, no output in collapsed mode) | | `truncated-tool.ts` | Wraps ripgrep with proper output truncation (50KB/2000 lines) | -| `antigravity-image-gen.ts` | Generate images via Google Antigravity with optional save-to-disk modes | | `ssh.ts` | Delegate all tools to a remote machine via SSH using pluggable operations | | `subagent/` | Delegate tasks to specialized subagents with isolated context windows | diff --git a/packages/coding-agent/examples/extensions/antigravity-image-gen.ts b/packages/coding-agent/examples/extensions/antigravity-image-gen.ts deleted file mode 100644 index bdc44d11..00000000 --- a/packages/coding-agent/examples/extensions/antigravity-image-gen.ts +++ /dev/null @@ -1,418 +0,0 @@ -/** - * Antigravity Image Generation - * - * Generates images via Google Antigravity's image models (gemini-3-pro-image, imagen-3). - * Returns images as tool result attachments for inline terminal rendering. - * Requires OAuth login via /login for google-antigravity. - * - * Usage: - * "Generate an image of a sunset over mountains" - * "Create a 16:9 wallpaper of a cyberpunk city" - * - * Save modes (tool param, env var, or config file): - * save=none - Don't save to disk (default) - * save=project - Save to /.pi/generated-images/ - * save=global - Save to ~/.pi/agent/generated-images/ - * save=custom - Save to saveDir param or PI_IMAGE_SAVE_DIR - * - * Environment variables: - * PI_IMAGE_SAVE_MODE - Default save mode (none|project|global|custom) - * PI_IMAGE_SAVE_DIR - Directory for custom save mode - * - * Config files (project overrides global): - * ~/.pi/agent/extensions/antigravity-image-gen.json - * /.pi/extensions/antigravity-image-gen.json - * Example: { "save": "global" } - */ - -import { randomUUID } from "node:crypto"; -import { existsSync, readFileSync } from "node:fs"; -import { mkdir, writeFile } from "node:fs/promises"; -import { join } from "node:path"; -import { StringEnum } from "@mariozechner/pi-ai"; -import { type ExtensionAPI, getAgentDir, withFileMutationQueue } from "@mariozechner/pi-coding-agent"; -import { type Static, Type } from "typebox"; - -const PROVIDER = "google-antigravity"; - -const ASPECT_RATIOS = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"] as const; - -type AspectRatio = (typeof ASPECT_RATIOS)[number]; - -const DEFAULT_MODEL = "gemini-3-pro-image"; -const DEFAULT_ASPECT_RATIO: AspectRatio = "1:1"; -const DEFAULT_SAVE_MODE = "none"; - -const SAVE_MODES = ["none", "project", "global", "custom"] as const; -type SaveMode = (typeof SAVE_MODES)[number]; - -const ANTIGRAVITY_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com"; - -const DEFAULT_ANTIGRAVITY_VERSION = "1.21.9"; - -const ANTIGRAVITY_HEADERS = { - "User-Agent": `antigravity/${process.env.PI_AI_ANTIGRAVITY_VERSION || DEFAULT_ANTIGRAVITY_VERSION} darwin/arm64`, - "X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1", - "Client-Metadata": JSON.stringify({ - ideType: "IDE_UNSPECIFIED", - platform: "PLATFORM_UNSPECIFIED", - pluginType: "GEMINI", - }), -}; - -const IMAGE_SYSTEM_INSTRUCTION = - "You are an AI image generator. Generate images based on user descriptions. Focus on creating high-quality, visually appealing images that match the user's request."; - -const TOOL_PARAMS = Type.Object({ - prompt: Type.String({ description: "Image description." }), - model: Type.Optional( - Type.String({ - description: "Image model id (e.g., gemini-3-pro-image, imagen-3). Default: gemini-3-pro-image.", - }), - ), - aspectRatio: Type.Optional(StringEnum(ASPECT_RATIOS)), - save: Type.Optional(StringEnum(SAVE_MODES)), - saveDir: Type.Optional( - Type.String({ - description: "Directory to save image when save=custom. Defaults to PI_IMAGE_SAVE_DIR if set.", - }), - ), -}); - -type ToolParams = Static; - -interface CloudCodeAssistRequest { - project: string; - model: string; - request: { - contents: Content[]; - sessionId?: string; - systemInstruction?: { role?: string; parts: { text: string }[] }; - generationConfig?: { - maxOutputTokens?: number; - temperature?: number; - imageConfig?: { aspectRatio?: string }; - candidateCount?: number; - }; - safetySettings?: Array<{ category: string; threshold: string }>; - }; - requestType?: string; - userAgent?: string; - requestId?: string; -} - -interface CloudCodeAssistResponseChunk { - response?: { - candidates?: Array<{ - content?: { - role: string; - parts?: Array<{ - text?: string; - inlineData?: { - mimeType?: string; - data?: string; - }; - }>; - }; - }>; - usageMetadata?: { - promptTokenCount?: number; - candidatesTokenCount?: number; - thoughtsTokenCount?: number; - totalTokenCount?: number; - cachedContentTokenCount?: number; - }; - modelVersion?: string; - responseId?: string; - }; - traceId?: string; -} - -interface Content { - role: "user" | "model"; - parts: Part[]; -} - -interface Part { - text?: string; - inlineData?: { - mimeType?: string; - data?: string; - }; -} - -interface ParsedCredentials { - accessToken: string; - projectId: string; -} - -interface ExtensionConfig { - save?: SaveMode; - saveDir?: string; -} - -interface SaveConfig { - mode: SaveMode; - outputDir?: string; -} - -function parseOAuthCredentials(raw: string): ParsedCredentials { - let parsed: { token?: string; projectId?: string }; - try { - parsed = JSON.parse(raw) as { token?: string; projectId?: string }; - } catch { - throw new Error("Invalid Google OAuth credentials. Run /login to re-authenticate."); - } - if (!parsed.token || !parsed.projectId) { - throw new Error("Missing token or projectId in Google OAuth credentials. Run /login."); - } - return { accessToken: parsed.token, projectId: parsed.projectId }; -} - -function readConfigFile(path: string): ExtensionConfig { - if (!existsSync(path)) { - return {}; - } - try { - const content = readFileSync(path, "utf-8"); - const parsed = JSON.parse(content) as ExtensionConfig; - return parsed ?? {}; - } catch { - return {}; - } -} - -function loadConfig(cwd: string): ExtensionConfig { - const globalPath = join(getAgentDir(), "extensions", "antigravity-image-gen.json"); - const globalConfig = readConfigFile(globalPath); - const projectConfig = readConfigFile(join(cwd, ".pi", "extensions", "antigravity-image-gen.json")); - return { ...globalConfig, ...projectConfig }; -} - -function resolveSaveConfig(params: ToolParams, cwd: string): SaveConfig { - const config = loadConfig(cwd); - const envMode = (process.env.PI_IMAGE_SAVE_MODE || "").toLowerCase(); - const paramMode = params.save; - const mode = (paramMode || envMode || config.save || DEFAULT_SAVE_MODE) as SaveMode; - - if (!SAVE_MODES.includes(mode)) { - return { mode: DEFAULT_SAVE_MODE as SaveMode }; - } - - if (mode === "project") { - return { mode, outputDir: join(cwd, ".pi", "generated-images") }; - } - - if (mode === "global") { - const outputDir = join(getAgentDir(), "generated-images"); - return { mode, outputDir }; - } - - if (mode === "custom") { - const dir = params.saveDir || process.env.PI_IMAGE_SAVE_DIR || config.saveDir; - if (!dir || !dir.trim()) { - throw new Error("save=custom requires saveDir or PI_IMAGE_SAVE_DIR."); - } - return { mode, outputDir: dir }; - } - - return { mode }; -} - -function imageExtension(mimeType: string): string { - const lower = mimeType.toLowerCase(); - if (lower.includes("jpeg") || lower.includes("jpg")) return "jpg"; - if (lower.includes("gif")) return "gif"; - if (lower.includes("webp")) return "webp"; - return "png"; -} - -async function saveImage(base64Data: string, mimeType: string, outputDir: string): Promise { - const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); - const ext = imageExtension(mimeType); - const filename = `image-${timestamp}-${randomUUID().slice(0, 8)}.${ext}`; - const filePath = join(outputDir, filename); - await withFileMutationQueue(filePath, async () => { - await mkdir(outputDir, { recursive: true }); - await writeFile(filePath, Buffer.from(base64Data, "base64")); - }); - return filePath; -} - -function buildRequest(prompt: string, model: string, projectId: string, aspectRatio: string): CloudCodeAssistRequest { - return { - project: projectId, - model, - request: { - contents: [ - { - role: "user", - parts: [{ text: prompt }], - }, - ], - systemInstruction: { - parts: [{ text: IMAGE_SYSTEM_INSTRUCTION }], - }, - generationConfig: { - imageConfig: { aspectRatio }, - candidateCount: 1, - }, - safetySettings: [ - { category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_ONLY_HIGH" }, - { category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_ONLY_HIGH" }, - { category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "BLOCK_ONLY_HIGH" }, - { category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "BLOCK_ONLY_HIGH" }, - { category: "HARM_CATEGORY_CIVIC_INTEGRITY", threshold: "BLOCK_ONLY_HIGH" }, - ], - }, - requestType: "agent", - requestId: `agent-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`, - userAgent: "antigravity", - }; -} - -async function parseSseForImage( - response: Response, - signal?: AbortSignal, -): Promise<{ image: { data: string; mimeType: string }; text: string[] }> { - if (!response.body) { - throw new Error("No response body"); - } - - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - let buffer = ""; - const textParts: string[] = []; - - try { - while (true) { - if (signal?.aborted) { - throw new Error("Request was aborted"); - } - - const { done, value } = await reader.read(); - if (done) break; - - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split("\n"); - buffer = lines.pop() || ""; - - for (const line of lines) { - if (!line.startsWith("data:")) continue; - const jsonStr = line.slice(5).trim(); - if (!jsonStr) continue; - - let chunk: CloudCodeAssistResponseChunk; - try { - chunk = JSON.parse(jsonStr) as CloudCodeAssistResponseChunk; - } catch { - continue; - } - - const responseData = chunk.response; - if (!responseData?.candidates) continue; - - for (const candidate of responseData.candidates) { - const parts = candidate.content?.parts; - if (!parts) continue; - for (const part of parts) { - if (part.text) { - textParts.push(part.text); - } - if (part.inlineData?.data) { - await reader.cancel(); - return { - image: { - data: part.inlineData.data, - mimeType: part.inlineData.mimeType || "image/png", - }, - text: textParts, - }; - } - } - } - } - } - } finally { - reader.releaseLock(); - } - - throw new Error("No image data returned by the model"); -} - -async function getCredentials(ctx: { - modelRegistry: { getApiKeyForProvider: (provider: string) => Promise }; -}): Promise { - const apiKey = await ctx.modelRegistry.getApiKeyForProvider(PROVIDER); - if (!apiKey) { - throw new Error("Missing Google Antigravity OAuth credentials. Run /login for google-antigravity."); - } - return parseOAuthCredentials(apiKey); -} - -export default function antigravityImageGen(pi: ExtensionAPI) { - pi.registerTool({ - name: "generate_image", - label: "Generate image", - description: - "Generate an image via Google Antigravity image models. Returns the image as a tool result attachment. Optional saving via save=project|global|custom|none, or PI_IMAGE_SAVE_MODE/PI_IMAGE_SAVE_DIR.", - parameters: TOOL_PARAMS, - async execute(_toolCallId, params: ToolParams, signal, onUpdate, ctx) { - const { accessToken, projectId } = await getCredentials(ctx); - const model = params.model || DEFAULT_MODEL; - const aspectRatio = params.aspectRatio || DEFAULT_ASPECT_RATIO; - - const requestBody = buildRequest(params.prompt, model, projectId, aspectRatio); - onUpdate?.({ - content: [{ type: "text", text: `Requesting image from ${PROVIDER}/${model}...` }], - details: { provider: PROVIDER, model, aspectRatio }, - }); - - const response = await fetch(`${ANTIGRAVITY_ENDPOINT}/v1internal:streamGenerateContent?alt=sse`, { - method: "POST", - headers: { - Authorization: `Bearer ${accessToken}`, - "Content-Type": "application/json", - Accept: "text/event-stream", - ...ANTIGRAVITY_HEADERS, - }, - body: JSON.stringify(requestBody), - signal, - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`Image request failed (${response.status}): ${errorText}`); - } - - const parsed = await parseSseForImage(response, signal); - const saveConfig = resolveSaveConfig(params, ctx.cwd); - let savedPath: string | undefined; - let saveError: string | undefined; - if (saveConfig.mode !== "none" && saveConfig.outputDir) { - try { - savedPath = await saveImage(parsed.image.data, parsed.image.mimeType, saveConfig.outputDir); - } catch (error) { - saveError = error instanceof Error ? error.message : String(error); - } - } - const summaryParts = [`Generated image via ${PROVIDER}/${model}.`, `Aspect ratio: ${aspectRatio}.`]; - if (savedPath) { - summaryParts.push(`Saved image to: ${savedPath}`); - } else if (saveError) { - summaryParts.push(`Failed to save image: ${saveError}`); - } - if (parsed.text.length > 0) { - summaryParts.push(`Model notes: ${parsed.text.join(" ")}`); - } - - return { - content: [ - { type: "text", text: summaryParts.join(" ") }, - { type: "image", data: parsed.image.data, mimeType: parsed.image.mimeType }, - ], - details: { provider: PROVIDER, model, aspectRatio, savedPath, saveMode: saveConfig.mode }, - }; - }, - }); -} diff --git a/packages/coding-agent/src/cli/args.ts b/packages/coding-agent/src/cli/args.ts index 7e08bbf4..de0a79d7 100644 --- a/packages/coding-agent/src/cli/args.ts +++ b/packages/coding-agent/src/cli/args.ts @@ -329,7 +329,6 @@ ${chalk.bold("Environment Variables:")} PI_OFFLINE - Disable startup network operations when set to 1/true/yes PI_TELEMETRY - Override install telemetry when set to 1/true/yes or 0/false/no PI_SHARE_VIEWER_URL - Base URL for /share command (default: https://pi.dev/session/) - PI_AI_ANTIGRAVITY_VERSION - Override Antigravity User-Agent version (e.g., 1.23.0) ${chalk.bold("Built-in Tool Names:")} read - Read file contents diff --git a/packages/coding-agent/src/core/model-resolver.ts b/packages/coding-agent/src/core/model-resolver.ts index b5ae536b..41ec9db7 100644 --- a/packages/coding-agent/src/core/model-resolver.ts +++ b/packages/coding-agent/src/core/model-resolver.ts @@ -19,8 +19,6 @@ export const defaultModelPerProvider: Record = { "openai-codex": "gpt-5.5", deepseek: "deepseek-v4-pro", google: "gemini-3.1-pro-preview", - "google-gemini-cli": "gemini-3.1-pro-preview", - "google-antigravity": "gemini-3.1-pro-high", "google-vertex": "gemini-3.1-pro-preview", "github-copilot": "gpt-5.4", openrouter: "moonshotai/kimi-k2.6", diff --git a/packages/coding-agent/test/compaction-thinking-model.test.ts b/packages/coding-agent/test/compaction-thinking-model.test.ts deleted file mode 100644 index 2417ca82..00000000 --- a/packages/coding-agent/test/compaction-thinking-model.test.ts +++ /dev/null @@ -1,223 +0,0 @@ -/** - * Test for compaction with thinking models. - * - * Tests both: - * - Claude via Antigravity (google-gemini-cli API) - * - Claude via real Anthropic API (anthropic-messages API) - * - * Reproduces issue where compact fails when maxTokens < thinkingBudget. - */ - -import { existsSync, mkdirSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { Agent, type ThinkingLevel } from "@mariozechner/pi-agent-core"; -import { getModel, type Model } from "@mariozechner/pi-ai"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -import { AgentSession } from "../src/core/agent-session.js"; -import { ModelRegistry } from "../src/core/model-registry.js"; -import { SessionManager } from "../src/core/session-manager.js"; -import { SettingsManager } from "../src/core/settings-manager.js"; -import { createCodingTools } from "../src/core/tools/index.js"; -import { - API_KEY, - createTestResourceLoader, - getRealAuthStorage, - hasAuthForProvider, - resolveApiKey, -} from "./utilities.js"; - -// Check for auth -const HAS_ANTIGRAVITY_AUTH = hasAuthForProvider("google-antigravity"); -const HAS_ANTHROPIC_AUTH = !!API_KEY; - -describe.skipIf(!HAS_ANTIGRAVITY_AUTH)("Compaction with thinking models (Antigravity)", () => { - let session: AgentSession; - let tempDir: string; - let apiKey: string; - - beforeAll(async () => { - const key = await resolveApiKey("google-antigravity"); - if (!key) throw new Error("Failed to resolve google-antigravity API key"); - apiKey = key; - }); - - beforeEach(() => { - tempDir = join(tmpdir(), `pi-thinking-compaction-test-${Date.now()}`); - mkdirSync(tempDir, { recursive: true }); - }); - - afterEach(async () => { - if (session) { - session.dispose(); - } - if (tempDir && existsSync(tempDir)) { - rmSync(tempDir, { recursive: true }); - } - }); - - function createSession( - modelId: "claude-opus-4-5-thinking" | "claude-sonnet-4-5", - thinkingLevel: ThinkingLevel = "high", - ) { - const model = getModel("google-antigravity", modelId); - if (!model) { - throw new Error(`Model not found: google-antigravity/${modelId}`); - } - - const agent = new Agent({ - getApiKey: () => apiKey, - initialState: { - model, - systemPrompt: "You are a helpful assistant. Be concise.", - tools: createCodingTools(process.cwd()), - thinkingLevel, - }, - }); - - const sessionManager = SessionManager.inMemory(); - const settingsManager = SettingsManager.create(tempDir, tempDir); - // Use minimal keepRecentTokens so small test conversations have something to summarize - // settingsManager.applyOverrides({ compaction: { keepRecentTokens: 1 } }); - - const authStorage = getRealAuthStorage(); - const modelRegistry = ModelRegistry.create(authStorage); - - session = new AgentSession({ - agent, - sessionManager, - settingsManager, - cwd: tempDir, - modelRegistry, - resourceLoader: createTestResourceLoader(), - }); - - session.subscribe(() => {}); - - return session; - } - - it("should compact successfully with claude-opus-4-5-thinking and thinking level high", async () => { - createSession("claude-opus-4-5-thinking", "high"); - - // Send a simple prompt - await session.prompt("Write down the first 10 prime numbers."); - await session.agent.waitForIdle(); - - // Verify we got a response - const messages = session.messages; - expect(messages.length).toBeGreaterThan(0); - - const assistantMessages = messages.filter((m) => m.role === "assistant"); - expect(assistantMessages.length).toBeGreaterThan(0); - - // Now try to compact - this should not throw - const result = await session.compact(); - - expect(result.summary).toBeDefined(); - expect(result.summary.length).toBeGreaterThan(0); - expect(result.tokensBefore).toBeGreaterThan(0); - - // Verify session is still usable after compaction - const messagesAfterCompact = session.messages; - expect(messagesAfterCompact.length).toBeGreaterThan(0); - expect(messagesAfterCompact[0].role).toBe("compactionSummary"); - }, 180000); - - it("should compact successfully with claude-sonnet-4-5 (non-thinking) for comparison", async () => { - createSession("claude-sonnet-4-5", "off"); - - await session.prompt("Write down the first 10 prime numbers."); - await session.agent.waitForIdle(); - - const messages = session.messages; - expect(messages.length).toBeGreaterThan(0); - - const result = await session.compact(); - - expect(result.summary).toBeDefined(); - expect(result.summary.length).toBeGreaterThan(0); - }, 180000); -}); - -// ============================================================================ -// Real Anthropic API tests (for comparison) -// ============================================================================ - -describe.skipIf(!HAS_ANTHROPIC_AUTH)("Compaction with thinking models (Anthropic)", () => { - let session: AgentSession; - let tempDir: string; - - beforeEach(() => { - tempDir = join(tmpdir(), `pi-thinking-compaction-anthropic-test-${Date.now()}`); - mkdirSync(tempDir, { recursive: true }); - }); - - afterEach(async () => { - if (session) { - session.dispose(); - } - if (tempDir && existsSync(tempDir)) { - rmSync(tempDir, { recursive: true }); - } - }); - - function createSession(model: Model, thinkingLevel: ThinkingLevel = "high") { - const agent = new Agent({ - getApiKey: () => API_KEY, - initialState: { - model, - systemPrompt: "You are a helpful assistant. Be concise.", - tools: createCodingTools(process.cwd()), - thinkingLevel, - }, - }); - - const sessionManager = SessionManager.inMemory(); - const settingsManager = SettingsManager.create(tempDir, tempDir); - - const authStorage = getRealAuthStorage(); - const modelRegistry = ModelRegistry.create(authStorage); - - session = new AgentSession({ - agent, - sessionManager, - settingsManager, - cwd: tempDir, - modelRegistry, - resourceLoader: createTestResourceLoader(), - }); - - session.subscribe(() => {}); - - return session; - } - - it("should compact successfully with claude-sonnet-4-5 and thinking level high", async () => { - const model = getModel("anthropic", "claude-sonnet-4-5")!; - createSession(model, "high"); - - // Send a simple prompt - await session.prompt("Write down the first 10 prime numbers."); - await session.agent.waitForIdle(); - - // Verify we got a response - const messages = session.messages; - expect(messages.length).toBeGreaterThan(0); - - const assistantMessages = messages.filter((m) => m.role === "assistant"); - expect(assistantMessages.length).toBeGreaterThan(0); - - // Now try to compact - this should not throw - const result = await session.compact(); - - expect(result.summary).toBeDefined(); - expect(result.summary.length).toBeGreaterThan(0); - expect(result.tokensBefore).toBeGreaterThan(0); - - // Verify session is still usable after compaction - const messagesAfterCompact = session.messages; - expect(messagesAfterCompact.length).toBeGreaterThan(0); - expect(messagesAfterCompact[0].role).toBe("compactionSummary"); - }, 180000); -}); diff --git a/packages/coding-agent/test/utilities.ts b/packages/coding-agent/test/utilities.ts index b5113be1..ea10fcb2 100644 --- a/packages/coding-agent/test/utilities.ts +++ b/packages/coding-agent/test/utilities.ts @@ -71,7 +71,6 @@ function saveAuthStorage(storage: AuthStorageData): void { * For API key credentials, returns the key directly. * For OAuth credentials, returns the access token (refreshing if expired and saving back). * - * For google-gemini-cli and google-antigravity, returns JSON-encoded { token, projectId } */ export async function resolveApiKey(provider: string): Promise { const storage = loadAuthStorage();