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
This commit is contained in:
Roo Code 2025-07-22 04:11:26 +00:00
parent b1bc085aa6
commit 41d96f13ad
10 changed files with 772 additions and 0 deletions

View file

@ -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<string, ModelInfo>

View file

@ -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"

View file

@ -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":

View file

@ -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
})
})
})

View file

@ -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<string, GeminiCliUsage> = 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<string> {
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<boolean> {
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<void> {
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
}
}

View file

@ -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"

View file

@ -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 = ({
<Gemini apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "gemini-cli" && (
<GeminiCli apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "openai" && (
<OpenAICompatible
apiConfiguration={apiConfiguration}

View file

@ -6,6 +6,7 @@ import {
claudeCodeModels,
deepSeekModels,
geminiModels,
geminiCliModels,
mistralModels,
openAiNativeModels,
vertexModels,
@ -20,6 +21,7 @@ export const MODELS_BY_PROVIDER: Partial<Record<ProviderName, Record<string, Mod
bedrock: bedrockModels,
deepseek: deepSeekModels,
gemini: geminiModels,
"gemini-cli": geminiCliModels,
mistral: mistralModels,
"openai-native": openAiNativeModels,
vertex: vertexModels,
@ -33,6 +35,7 @@ export const PROVIDERS = [
{ value: "anthropic", label: "Anthropic" },
{ value: "claude-code", label: "Claude Code" },
{ value: "gemini", label: "Google Gemini" },
{ value: "gemini-cli", label: "Google Gemini CLI" },
{ value: "deepseek", label: "DeepSeek" },
{ value: "openai-native", label: "OpenAI" },
{ value: "openai", label: "OpenAI Compatible" },

View file

@ -0,0 +1,63 @@
import React from "react"
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import type { ProviderSettings } from "@roo-code/types"
import { buildDocLink } from "@src/utils/docLinks"
interface GeminiCliProps {
apiConfiguration: ProviderSettings
setApiConfigurationField: <K extends keyof ProviderSettings>(field: K, value: ProviderSettings[K]) => void
}
export const GeminiCli: React.FC<GeminiCliProps> = ({ apiConfiguration, setApiConfigurationField }) => {
return (
<>
<div className="text-sm text-vscode-descriptionForeground">
Use Google Gemini models through the Gemini CLI, which provides free access to Gemini Pro through Google
Code Assist.
</div>
<div className="text-sm text-vscode-descriptionForeground">
<VSCodeLink href={buildDocLink("providers/gemini-cli", "provider_docs")} target="_blank">
View setup instructions
</VSCodeLink>
</div>
<div>
<label className="block font-medium mb-1">Project ID (Optional)</label>
<input
type="text"
value={apiConfiguration.geminiCliProjectId || ""}
onChange={(e) => 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"
/>
<div className="text-xs text-vscode-descriptionForeground mt-1">
For paid Google Cloud accounts. Leave empty for free tier access.
</div>
</div>
<div className="p-3 bg-vscode-editorWidget-background border border-vscode-editorWidget-border rounded">
<h4 className="font-medium mb-2">Authentication</h4>
<div className="text-sm text-vscode-descriptionForeground space-y-2">
<p>
1. Install the Gemini CLI: <code>npm install -g @google/generative-ai-cli</code>
</p>
<p>2. When you send your first message, a browser window will open for Google authentication</p>
<p>3. After authenticating, your session will be saved for future use</p>
</div>
</div>
<div className="p-3 bg-vscode-editorWidget-background border border-vscode-editorWidget-border rounded">
<h4 className="font-medium mb-2">Features</h4>
<ul className="text-sm text-vscode-descriptionForeground space-y-1 list-disc list-inside">
<li>Free access to Gemini Pro models through Google Code Assist</li>
<li>Automatic OAuth authentication flow</li>
<li>Built-in telemetry for token usage tracking</li>
<li>Support for advanced features like debug mode and IDE mode</li>
</ul>
</div>
</>
)
}

View file

@ -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"