From 41d96f13ade00bfefb88a8d2d726c6df6a1ed267 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Tue, 22 Jul 2025 04:11:26 +0000 Subject: [PATCH] feat: add Gemini-CLI provider support - Add GeminiCliHandler class to interface with gemini CLI tool - Implement OAuth authentication flow handling - Add telemetry collection for token usage tracking - Create UI component for provider configuration - Add comprehensive test coverage for the provider - Support both free and paid (project ID) usage modes Fixes #6043 --- packages/types/src/providers/gemini-cli.ts | 52 +++ packages/types/src/providers/index.ts | 1 + src/api/index.ts | 3 + .../providers/__tests__/gemini-cli.spec.ts | 330 ++++++++++++++++++ src/api/providers/gemini-cli.ts | 311 +++++++++++++++++ src/api/providers/index.ts | 1 + .../src/components/settings/ApiOptions.tsx | 7 + .../src/components/settings/constants.ts | 3 + .../settings/providers/GeminiCli.tsx | 63 ++++ .../components/settings/providers/index.ts | 1 + 10 files changed, 772 insertions(+) create mode 100644 packages/types/src/providers/gemini-cli.ts create mode 100644 src/api/providers/__tests__/gemini-cli.spec.ts create mode 100644 src/api/providers/gemini-cli.ts create mode 100644 webview-ui/src/components/settings/providers/GeminiCli.tsx diff --git a/packages/types/src/providers/gemini-cli.ts b/packages/types/src/providers/gemini-cli.ts new file mode 100644 index 0000000000..2b4be368a0 --- /dev/null +++ b/packages/types/src/providers/gemini-cli.ts @@ -0,0 +1,52 @@ +import type { ModelInfo } from "../model.js" + +// Gemini-CLI specific model IDs +export type GeminiCliModelId = keyof typeof geminiCliModels + +export const geminiCliDefaultModelId: GeminiCliModelId = "gemini-2.0-flash-001" + +// Models available through Gemini CLI +export const geminiCliModels = { + "gemini-2.0-flash-001": { + maxTokens: 8192, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 0.1, + outputPrice: 0.4, + cacheReadsPrice: 0.025, + cacheWritesPrice: 1.0, + }, + "gemini-1.5-flash-002": { + maxTokens: 8192, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 0.15, + outputPrice: 0.6, + cacheReadsPrice: 0.0375, + cacheWritesPrice: 1.0, + tiers: [ + { + contextWindow: 128_000, + inputPrice: 0.075, + outputPrice: 0.3, + cacheReadsPrice: 0.01875, + }, + { + contextWindow: Infinity, + inputPrice: 0.15, + outputPrice: 0.6, + cacheReadsPrice: 0.0375, + }, + ], + }, + "gemini-1.5-pro-002": { + maxTokens: 8192, + contextWindow: 2_097_152, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + }, +} as const satisfies Record diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index 267401bd91..0f3e96fdcb 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -4,6 +4,7 @@ export * from "./chutes.js" export * from "./claude-code.js" export * from "./deepseek.js" export * from "./gemini.js" +export * from "./gemini-cli.js" export * from "./glama.js" export * from "./groq.js" export * from "./lite-llm.js" diff --git a/src/api/index.ts b/src/api/index.ts index 960209f714..e589d70ea1 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -28,6 +28,7 @@ import { ChutesHandler, LiteLLMHandler, ClaudeCodeHandler, + GeminiCliHandler, } from "./providers" export interface SingleCompletionHandler { @@ -85,6 +86,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { return new LmStudioHandler(options) case "gemini": return new GeminiHandler(options) + case "gemini-cli": + return new GeminiCliHandler(options) case "openai-native": return new OpenAiNativeHandler(options) case "deepseek": diff --git a/src/api/providers/__tests__/gemini-cli.spec.ts b/src/api/providers/__tests__/gemini-cli.spec.ts new file mode 100644 index 0000000000..8aae9b2087 --- /dev/null +++ b/src/api/providers/__tests__/gemini-cli.spec.ts @@ -0,0 +1,330 @@ +// npx vitest run src/api/providers/__tests__/gemini-cli.spec.ts + +import { describe, it, expect, vi, beforeEach } from "vitest" +import { spawn } from "child_process" +import { EventEmitter } from "events" + +import { GeminiCliHandler } from "../gemini-cli" +import type { ApiHandlerOptions } from "../../../shared/api" + +// Mock child_process +vi.mock("child_process", () => ({ + spawn: vi.fn(), +})) + +describe("GeminiCliHandler", () => { + let handler: GeminiCliHandler + let mockOptions: ApiHandlerOptions + + beforeEach(() => { + vi.clearAllMocks() + mockOptions = { + apiModelId: "gemini-2.0-flash-001", + modelTemperature: 0.7, + geminiCliProjectId: "test-project", + } + handler = new GeminiCliHandler(mockOptions) + }) + + describe("getModel", () => { + it("should return the correct model configuration", () => { + const model = handler.getModel() + expect(model.id).toBe("gemini-2.0-flash-001") + expect(model.info).toBeDefined() + expect(model.info.maxTokens).toBe(8192) + expect(model.info.contextWindow).toBe(1_048_576) + }) + + it("should use default model when apiModelId is not provided", () => { + handler = new GeminiCliHandler({}) + const model = handler.getModel() + expect(model.id).toBe("gemini-2.0-flash-001") + }) + }) + + describe("completePrompt", () => { + it("should execute gemini CLI and return response", async () => { + const mockProcess = new EventEmitter() as any + mockProcess.stdout = new EventEmitter() + mockProcess.stderr = new EventEmitter() + + vi.mocked(spawn).mockReturnValue(mockProcess) + + const promptPromise = handler.completePrompt("Test prompt") + + // Simulate CLI response + mockProcess.stdout.emit("data", JSON.stringify({ text: "Test response" })) + mockProcess.emit("close", 0) + + const result = await promptPromise + expect(result).toBe("Test response") + expect(spawn).toHaveBeenCalledWith( + "gemini", + expect.arrayContaining([ + "prompt", + "Test prompt", + "--model", + "gemini-2.0-flash-001", + "--project", + "test-project", + "--temperature", + "0.7", + "--json", + ]), + expect.any(Object), + ) + }) + + it("should handle CLI errors", async () => { + const mockProcess = new EventEmitter() as any + mockProcess.stdout = new EventEmitter() + mockProcess.stderr = new EventEmitter() + + vi.mocked(spawn).mockReturnValue(mockProcess) + + const promptPromise = handler.completePrompt("Test prompt") + + // Simulate CLI error + mockProcess.stderr.emit("data", "Error message") + mockProcess.emit("close", 1) + + await expect(promptPromise).rejects.toThrow("Gemini CLI failed with code 1") + }) + }) + + describe("createMessage", () => { + it("should handle authentication flow when not authenticated", async () => { + // Mock auth check to fail + const mockAuthProcess = new EventEmitter() as any + mockAuthProcess.stdout = new EventEmitter() + mockAuthProcess.stderr = new EventEmitter() + + // Mock OAuth flow + const mockOAuthProcess = new EventEmitter() as any + mockOAuthProcess.stdout = new EventEmitter() + mockOAuthProcess.stderr = new EventEmitter() + + vi.mocked(spawn) + .mockReturnValueOnce(mockAuthProcess) // auth status check + .mockReturnValueOnce(mockOAuthProcess) // auth login + + const messages = [{ role: "user" as const, content: "Hello" }] + const generator = handler.createMessage("System prompt", messages) + + // Simulate auth check failure immediately + setImmediate(() => { + mockAuthProcess.emit("close", 1) + }) + + // Simulate OAuth success + setImmediate(() => { + mockOAuthProcess.emit("close", 0) + }) + + const results = [] + for await (const chunk of generator) { + results.push(chunk) + } + + expect(results[0]).toEqual({ + type: "text", + text: "Please authenticate with Google in your browser. Once authenticated, please retry your request.", + }) + }) + + it("should process messages and return response with usage", async () => { + // Mock successful auth check + const mockAuthProcess = new EventEmitter() as any + mockAuthProcess.stdout = new EventEmitter() + mockAuthProcess.stderr = new EventEmitter() + + // Mock gemini execution + const mockGeminiProcess = new EventEmitter() as any + mockGeminiProcess.stdout = new EventEmitter() + mockGeminiProcess.stderr = new EventEmitter() + + vi.mocked(spawn) + .mockReturnValueOnce(mockAuthProcess) // auth status check + .mockReturnValueOnce(mockGeminiProcess) // gemini prompt + + const messages = [{ role: "user" as const, content: "Hello" }] + const generator = handler.createMessage("System prompt", messages) + + // Simulate successful auth + setImmediate(() => { + mockAuthProcess.emit("close", 0) + }) + + // Simulate gemini response with telemetry + setImmediate(() => { + mockGeminiProcess.stderr.emit("data", "Input tokens: 100\nOutput tokens: 50") + mockGeminiProcess.stdout.emit("data", JSON.stringify({ text: "Hello response" })) + mockGeminiProcess.emit("close", 0) + }) + + const results = [] + for await (const chunk of generator) { + results.push(chunk) + } + + expect(results).toHaveLength(2) + expect(results[0]).toEqual({ + type: "text", + text: "Hello response", + }) + expect(results[1]).toMatchObject({ + type: "usage", + inputTokens: 100, + outputTokens: 50, + }) + }) + + it("should format complex messages correctly", async () => { + // Mock successful auth and execution + const mockAuthProcess = new EventEmitter() as any + mockAuthProcess.stdout = new EventEmitter() + mockAuthProcess.stderr = new EventEmitter() + + const mockGeminiProcess = new EventEmitter() as any + mockGeminiProcess.stdout = new EventEmitter() + mockGeminiProcess.stderr = new EventEmitter() + + vi.mocked(spawn).mockReturnValueOnce(mockAuthProcess).mockReturnValueOnce(mockGeminiProcess) + + const messages = [ + { role: "user" as const, content: "Hello" }, + { role: "assistant" as const, content: "Hi there!" }, + { + role: "user" as const, + content: [ + { type: "text" as const, text: "Check this image" }, + { + type: "image" as const, + source: { type: "base64" as const, media_type: "image/png" as const, data: "base64data" }, + }, + ], + }, + ] + + const generator = handler.createMessage("System prompt", messages) + + // Simulate successful auth + setImmediate(() => { + mockAuthProcess.emit("close", 0) + }) + + // Simulate gemini response + setImmediate(() => { + // Check the formatted prompt after auth completes + const spawnCalls = vi.mocked(spawn).mock.calls + if (spawnCalls.length > 1) { + const callArgs = spawnCalls[1][1] + const promptArg = callArgs[1] // The prompt is the second argument + + expect(promptArg).toContain("System prompt") + expect(promptArg).toContain("User: Hello") + expect(promptArg).toContain("Assistant: Hi there!") + expect(promptArg).toContain("User: Check this image") + expect(promptArg).toContain("[Image provided]") + } + + mockGeminiProcess.stdout.emit("data", JSON.stringify({ text: "Response" })) + mockGeminiProcess.emit("close", 0) + }) + + const results = [] + for await (const chunk of generator) { + results.push(chunk) + } + + expect(results[0]).toEqual({ + type: "text", + text: "Response", + }) + }) + }) + + describe("telemetry parsing", () => { + it("should parse token usage from stderr output", async () => { + const mockAuthProcess = new EventEmitter() as any + mockAuthProcess.stdout = new EventEmitter() + mockAuthProcess.stderr = new EventEmitter() + + const mockGeminiProcess = new EventEmitter() as any + mockGeminiProcess.stdout = new EventEmitter() + mockGeminiProcess.stderr = new EventEmitter() + + vi.mocked(spawn).mockReturnValueOnce(mockAuthProcess).mockReturnValueOnce(mockGeminiProcess) + + const messages = [{ role: "user" as const, content: "Test" }] + const generator = handler.createMessage("System prompt", messages) + + // Simulate successful auth + setImmediate(() => { + mockAuthProcess.emit("close", 0) + }) + + // Simulate various telemetry outputs + setImmediate(() => { + mockGeminiProcess.stderr.emit("data", "Input tokens: 150") + mockGeminiProcess.stderr.emit("data", "Output tokens: 75") + mockGeminiProcess.stderr.emit("data", "Cache read tokens: 25") + mockGeminiProcess.stderr.emit("data", "Cache write tokens: 10") + mockGeminiProcess.stdout.emit("data", JSON.stringify({ text: "Response" })) + mockGeminiProcess.emit("close", 0) + }) + + const results = [] + for await (const chunk of generator) { + results.push(chunk) + } + + const usageChunk = results.find((r) => r.type === "usage") + expect(usageChunk).toMatchObject({ + type: "usage", + inputTokens: 150, + outputTokens: 75, + cacheReadTokens: 25, + cacheWriteTokens: 10, + }) + }) + }) + + describe("cost calculation", () => { + it("should calculate cost correctly", async () => { + const mockAuthProcess = new EventEmitter() as any + mockAuthProcess.stdout = new EventEmitter() + mockAuthProcess.stderr = new EventEmitter() + + const mockGeminiProcess = new EventEmitter() as any + mockGeminiProcess.stdout = new EventEmitter() + mockGeminiProcess.stderr = new EventEmitter() + + vi.mocked(spawn).mockReturnValueOnce(mockAuthProcess).mockReturnValueOnce(mockGeminiProcess) + + const messages = [{ role: "user" as const, content: "Test" }] + const generator = handler.createMessage("System prompt", messages) + + // Simulate successful auth + setImmediate(() => { + mockAuthProcess.emit("close", 0) + }) + + // Simulate telemetry with known values + setImmediate(() => { + mockGeminiProcess.stderr.emit("data", "Input tokens: 1000000") // 1M tokens + mockGeminiProcess.stderr.emit("data", "Output tokens: 1000000") // 1M tokens + mockGeminiProcess.stdout.emit("data", JSON.stringify({ text: "Response" })) + mockGeminiProcess.emit("close", 0) + }) + + const results = [] + for await (const chunk of generator) { + results.push(chunk) + } + + const usageChunk = results.find((r) => r.type === "usage") + expect(usageChunk?.totalCost).toBe(0.5) // 0.1 + 0.4 = 0.5 for 1M input + 1M output + }) + }) +}) diff --git a/src/api/providers/gemini-cli.ts b/src/api/providers/gemini-cli.ts new file mode 100644 index 0000000000..578e13bedf --- /dev/null +++ b/src/api/providers/gemini-cli.ts @@ -0,0 +1,311 @@ +import { spawn, ChildProcess } from "child_process" +import { Anthropic } from "@anthropic-ai/sdk" +import * as vscode from "vscode" + +import { type ModelInfo, type GeminiCliModelId, geminiCliDefaultModelId, geminiCliModels } from "@roo-code/types" + +import type { ApiHandlerOptions } from "../../shared/api" + +import { ApiStream } from "../transform/stream" +import { getModelParams } from "../transform/model-params" + +import { BaseProvider } from "./base-provider" +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" + +interface GeminiCliUsage { + inputTokens: number + outputTokens: number + cacheReadTokens?: number + cacheWriteTokens?: number +} + +export class GeminiCliHandler extends BaseProvider implements SingleCompletionHandler { + private options: ApiHandlerOptions + private telemetryCollector: Map = new Map() + private currentRequestId: string | null = null + + constructor(options: ApiHandlerOptions) { + super() + this.options = options + } + + async *createMessage( + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], + metadata?: ApiHandlerCreateMessageMetadata, + ): ApiStream { + const { id: modelId, info } = this.getModel() + + // Generate a unique request ID for telemetry tracking + this.currentRequestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` + + // Initialize usage tracking for this request + this.telemetryCollector.set(this.currentRequestId, { + inputTokens: 0, + outputTokens: 0, + cacheReadTokens: 0, + cacheWriteTokens: 0, + }) + + try { + // Prepare the prompt combining system prompt and messages + const fullPrompt = this.formatPrompt(systemPrompt, messages) + + // Build the command arguments + const args = this.buildCommandArgs(modelId, fullPrompt) + + // Check if user needs to authenticate + const needsAuth = await this.checkAuthentication() + if (needsAuth) { + yield { + type: "text", + text: "Please authenticate with Google in your browser. Once authenticated, please retry your request.", + } + + // Trigger OAuth flow + await this.triggerOAuthFlow() + return + } + + // Execute the gemini CLI command + const { text, usage } = await this.executeGeminiCli(args) + + // Yield the response text + yield { + type: "text", + text, + } + + // Yield usage information + if (usage) { + yield { + type: "usage", + inputTokens: usage.inputTokens, + outputTokens: usage.outputTokens, + cacheReadTokens: usage.cacheReadTokens, + cacheWriteTokens: usage.cacheWriteTokens, + totalCost: this.calculateCost(info, usage), + } + } + } finally { + // Clean up telemetry data for this request + if (this.currentRequestId) { + this.telemetryCollector.delete(this.currentRequestId) + this.currentRequestId = null + } + } + } + + override getModel() { + const modelId = this.options.apiModelId + let id = modelId && modelId in geminiCliModels ? (modelId as GeminiCliModelId) : geminiCliDefaultModelId + const info: ModelInfo = geminiCliModels[id] + const params = getModelParams({ format: "gemini", modelId: id, model: info, settings: this.options }) + + return { id, info, ...params } + } + + async completePrompt(prompt: string): Promise { + const { id: modelId } = this.getModel() + const args = this.buildCommandArgs(modelId, prompt) + + const { text } = await this.executeGeminiCli(args) + return text + } + + private formatPrompt(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): string { + let fullPrompt = systemPrompt + "\n\n" + + for (const message of messages) { + if (message.role === "user") { + fullPrompt += "User: " + } else if (message.role === "assistant") { + fullPrompt += "Assistant: " + } + + if (typeof message.content === "string") { + fullPrompt += message.content + "\n\n" + } else if (Array.isArray(message.content)) { + for (const content of message.content) { + if (content.type === "text") { + fullPrompt += content.text + "\n" + } else if (content.type === "image") { + fullPrompt += "[Image provided]\n" + } + } + fullPrompt += "\n" + } + } + + return fullPrompt.trim() + } + + private buildCommandArgs(modelId: string, prompt: string): string[] { + const args = ["prompt", prompt, "--model", modelId] + + // Add project ID if configured + if (this.options.geminiCliProjectId) { + args.push("--project", this.options.geminiCliProjectId) + } + + // Add temperature if configured + if (this.options.modelTemperature !== undefined && this.options.modelTemperature !== null) { + args.push("--temperature", this.options.modelTemperature.toString()) + } + + // Add max tokens if configured + if (this.options.modelMaxTokens) { + args.push("--max-output-tokens", this.options.modelMaxTokens.toString()) + } + + // Enable JSON output for easier parsing + args.push("--json") + + return args + } + + private async checkAuthentication(): Promise { + return new Promise((resolve) => { + const checkAuth = spawn("gemini", ["auth", "status"], { + env: { ...process.env }, + }) + + checkAuth.on("close", (code) => { + // If auth check fails, user needs to authenticate + resolve(code !== 0) + }) + + checkAuth.on("error", () => { + // If command fails, assume auth is needed + resolve(true) + }) + }) + } + + private async triggerOAuthFlow(): Promise { + return new Promise((resolve, reject) => { + const authProcess = spawn("gemini", ["auth", "login"], { + env: { ...process.env }, + }) + + authProcess.on("close", (code) => { + if (code === 0) { + resolve() + } else { + reject(new Error("Authentication failed")) + } + }) + + authProcess.on("error", (error) => { + reject(error) + }) + }) + } + + private async executeGeminiCli(args: string[]): Promise<{ text: string; usage?: GeminiCliUsage }> { + return new Promise((resolve, reject) => { + let stdout = "" + let stderr = "" + + const geminiProcess = spawn("gemini", args, { + env: { ...process.env }, + }) + + geminiProcess.stdout.on("data", (data) => { + stdout += data.toString() + }) + + geminiProcess.stderr.on("data", (data) => { + stderr += data.toString() + + // Try to parse telemetry data from stderr + this.parseTelemetryFromOutput(data.toString()) + }) + + geminiProcess.on("close", (code) => { + if (code !== 0) { + reject(new Error(`Gemini CLI failed with code ${code}: ${stderr}`)) + return + } + + try { + // Parse JSON response + const response = JSON.parse(stdout) + + // Extract text and usage + const text = response.text || response.content || "" + const usage = this.currentRequestId ? this.telemetryCollector.get(this.currentRequestId) : undefined + + resolve({ text, usage }) + } catch (error) { + // Fallback to plain text if JSON parsing fails + resolve({ text: stdout.trim() }) + } + }) + + geminiProcess.on("error", (error) => { + reject(error) + }) + }) + } + + private parseTelemetryFromOutput(output: string): void { + if (!this.currentRequestId) return + + // Look for token usage patterns in the output + // This is a simplified example - actual implementation would depend on gemini CLI output format + const patterns = { + inputTokens: /Input tokens:\s*(\d+)/i, + outputTokens: /Output tokens:\s*(\d+)/i, + cacheRead: /Cache read tokens:\s*(\d+)/i, + cacheWrite: /Cache write tokens:\s*(\d+)/i, + } + + const usage = this.telemetryCollector.get(this.currentRequestId) + if (!usage) return + + for (const [key, pattern] of Object.entries(patterns)) { + const match = output.match(pattern) + if (match) { + const value = parseInt(match[1], 10) + switch (key) { + case "inputTokens": + usage.inputTokens = value + break + case "outputTokens": + usage.outputTokens = value + break + case "cacheRead": + usage.cacheReadTokens = value + break + case "cacheWrite": + usage.cacheWriteTokens = value + break + } + } + } + + this.telemetryCollector.set(this.currentRequestId, usage) + } + + private calculateCost(info: ModelInfo, usage: GeminiCliUsage): number | undefined { + if (!info.inputPrice || !info.outputPrice) { + return undefined + } + + const inputCost = (usage.inputTokens / 1_000_000) * info.inputPrice + const outputCost = (usage.outputTokens / 1_000_000) * info.outputPrice + + let cacheReadCost = 0 + if (usage.cacheReadTokens && info.cacheReadsPrice) { + cacheReadCost = (usage.cacheReadTokens / 1_000_000) * info.cacheReadsPrice + } + + let cacheWriteCost = 0 + if (usage.cacheWriteTokens && info.cacheWritesPrice) { + cacheWriteCost = (usage.cacheWriteTokens / 1_000_000) * info.cacheWritesPrice + } + + return inputCost + outputCost + cacheReadCost + cacheWriteCost + } +} diff --git a/src/api/providers/index.ts b/src/api/providers/index.ts index 93df5c58ff..5bad9482a0 100644 --- a/src/api/providers/index.ts +++ b/src/api/providers/index.ts @@ -6,6 +6,7 @@ export { ClaudeCodeHandler } from "./claude-code" export { DeepSeekHandler } from "./deepseek" export { FakeAIHandler } from "./fake-ai" export { GeminiHandler } from "./gemini" +export { GeminiCliHandler } from "./gemini-cli" export { GlamaHandler } from "./glama" export { GroqHandler } from "./groq" export { HumanRelayHandler } from "./human-relay" diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 681268a787..a99fe350b7 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -17,6 +17,7 @@ import { anthropicDefaultModelId, claudeCodeDefaultModelId, geminiDefaultModelId, + geminiCliDefaultModelId, deepSeekDefaultModelId, mistralDefaultModelId, xaiDefaultModelId, @@ -56,6 +57,7 @@ import { ClaudeCode, DeepSeek, Gemini, + GeminiCli, Glama, Groq, LMStudio, @@ -286,6 +288,7 @@ const ApiOptions = ({ "claude-code": { field: "apiModelId", default: claudeCodeDefaultModelId }, "openai-native": { field: "apiModelId", default: openAiNativeDefaultModelId }, gemini: { field: "apiModelId", default: geminiDefaultModelId }, + "gemini-cli": { field: "apiModelId", default: geminiCliDefaultModelId }, deepseek: { field: "apiModelId", default: deepSeekDefaultModelId }, mistral: { field: "apiModelId", default: mistralDefaultModelId }, xai: { field: "apiModelId", default: xaiDefaultModelId }, @@ -447,6 +450,10 @@ const ApiOptions = ({ )} + {selectedProvider === "gemini-cli" && ( + + )} + {selectedProvider === "openai" && ( (field: K, value: ProviderSettings[K]) => void +} + +export const GeminiCli: React.FC = ({ apiConfiguration, setApiConfigurationField }) => { + return ( + <> +
+ Use Google Gemini models through the Gemini CLI, which provides free access to Gemini Pro through Google + Code Assist. +
+ +
+ + View setup instructions + +
+ +
+ + setApiConfigurationField("geminiCliProjectId", e.target.value)} + className="w-full px-3 py-1.5 bg-vscode-input text-vscode-foreground border border-vscode-inputBorder rounded focus:outline-none focus:border-vscode-focusBorder" + placeholder="your-gcp-project-id" + /> +
+ For paid Google Cloud accounts. Leave empty for free tier access. +
+
+ +
+

Authentication

+
+

+ 1. Install the Gemini CLI: npm install -g @google/generative-ai-cli +

+

2. When you send your first message, a browser window will open for Google authentication

+

3. After authenticating, your session will be saved for future use

+
+
+ +
+

Features

+
    +
  • Free access to Gemini Pro models through Google Code Assist
  • +
  • Automatic OAuth authentication flow
  • +
  • Built-in telemetry for token usage tracking
  • +
  • Support for advanced features like debug mode and IDE mode
  • +
+
+ + ) +} diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index b195607430..4c7b3c9f0b 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -4,6 +4,7 @@ export { Chutes } from "./Chutes" export { ClaudeCode } from "./ClaudeCode" export { DeepSeek } from "./DeepSeek" export { Gemini } from "./Gemini" +export { GeminiCli } from "./GeminiCli" export { Glama } from "./Glama" export { Groq } from "./Groq" export { LMStudio } from "./LMStudio"