Compare commits

...

3 Commits

Author SHA1 Message Date
Cline Evaluation bac990e5ad Adding Gemini CLI 2025-06-25 12:56:48 -06:00
Cline Evaluation a694fa27eb Adding Gemini CLI 2025-06-25 12:17:35 -06:00
Cline Evaluation 9edbccc54f Adding Gemini CLI 2025-06-25 11:36:02 -06:00
12 changed files with 348 additions and 3 deletions
+2 -1
View File
@@ -37,4 +37,5 @@ src/hosts/vscode/*/methods.ts
src/hosts/vscode/*/index.ts
src/hosts/vscode/client/host-grpc-client.ts
src/hosts/vscode/host-grpc-service-config.ts
src/standalone/server-setup.ts
src/standalone/server-setup.ts
gemini-cli
+93
View File
@@ -0,0 +1,93 @@
# Gemini CLI Provider for Cline
This document describes how to use the Gemini CLI as a provider in Cline.
## Overview
The Gemini CLI provider allows you to use Google's Gemini models through the Gemini CLI tool instead of using API keys directly. This can be useful if you:
- Already have the Gemini CLI installed and configured
- Want to use the CLI's authentication methods (OAuth, etc.)
- Prefer to manage your Gemini access through the CLI
## Prerequisites
1. **Install the Gemini CLI**: The Gemini CLI must be installed and accessible from your system PATH.
2. **Set up authentication**: The CLI needs to be authenticated. You can either:
- Set the `GEMINI_API_KEY` environment variable
- Use the CLI's built-in authentication methods
3. **Build the CLI** (if using from source):
```bash
cd gemini-cli/packages/cli
npm install
npm run build
```
## Configuration
To use the Gemini CLI provider in Cline:
1. Open Cline settings
2. Select "Gemini CLI" as your API provider
3. (Optional) Specify the path to the Gemini CLI executable if it's not in your PATH
4. Select your desired Gemini model
## Supported Models
The Gemini CLI provider supports the same models as the regular Gemini provider:
- gemini-2.5-pro
- gemini-2.5-flash
- gemini-2.0-flash-001
- gemini-1.5-flash-002
- gemini-1.5-pro-002
## How It Works
1. When you send a message in Cline, it converts your conversation to a format the Gemini CLI understands
2. Cline spawns the Gemini CLI as a subprocess with your prompt
3. The CLI's response is streamed back to Cline
4. The response is displayed in the Cline interface
## Limitations
- **No image support**: The Gemini CLI in non-interactive mode doesn't support images
- **No tool execution**: The CLI handles its own tool execution, which doesn't integrate with Cline's tools
- **Token counting**: Token usage is estimated based on text length rather than actual token counts
- **No caching**: The CLI doesn't support Anthropic-style prompt caching
## Troubleshooting
### CLI not found
- Ensure the Gemini CLI is installed and in your PATH
- Or specify the full path to the CLI in Cline settings
### Authentication errors
- Check that `GEMINI_API_KEY` is set in your environment
- Or ensure the CLI is authenticated using its built-in methods
### No response
- Try running the CLI manually to ensure it works:
```bash
gemini --prompt "Hello, world!"
```
### Build errors
If using from source, ensure the CLI is built:
```bash
cd gemini-cli/packages/cli
npm install
npm run build
```
## Testing
You can test the integration using the provided test script:
```bash
node test-gemini-cli.js
```
This will attempt to run a simple prompt through the CLI and report whether it succeeded.
+3 -1
View File
@@ -123,6 +123,7 @@ enum ApiProvider {
CEREBRAS = 23;
SAPAICORE = 24;
CLAUDE_CODE = 25;
GEMINI_CLI = 26;
}
// Model info for OpenAI-compatible models
@@ -236,4 +237,5 @@ message ModelsApiConfiguration {
optional string sap_ai_core_token_url = 71;
optional string sap_ai_core_base_url = 72;
optional string claude_code_path = 73;
}
optional string gemini_cli_path = 74;
}
+3
View File
@@ -27,6 +27,7 @@ import { SambanovaHandler } from "./providers/sambanova"
import { CerebrasHandler } from "./providers/cerebras"
import { SapAiCoreHandler } from "./providers/sapaicore"
import { ClaudeCodeHandler } from "./providers/claude-code"
import { GeminiCliHandler } from "./providers/gemini-cli"
export interface ApiHandler {
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
@@ -92,6 +93,8 @@ function createHandlerForProvider(apiProvider: string | undefined, options: any)
return new SapAiCoreHandler(options)
case "claude-code":
return new ClaudeCodeHandler(options)
case "gemini-cli":
return new GeminiCliHandler(options)
default:
return new AnthropicHandler(options)
}
+83
View File
@@ -0,0 +1,83 @@
import type { Anthropic } from "@anthropic-ai/sdk"
import { geminiDefaultModelId, GeminiModelId, geminiModels, type ApiHandlerOptions } from "@/shared/api"
import { type ApiHandler } from ".."
import { type ApiStream } from "../transform/stream"
import { withRetry } from "../retry"
import { runGeminiCli } from "@/integrations/gemini-cli/run"
import { convertAnthropicToGeminiCliFormat } from "@/integrations/gemini-cli/message-converter"
export class GeminiCliHandler implements ApiHandler {
private options: ApiHandlerOptions
constructor(options: ApiHandlerOptions) {
this.options = options
}
@withRetry({
maxRetries: 4,
baseDelay: 2000,
maxDelay: 15000,
})
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
// Convert messages to Gemini CLI format
const prompt = convertAnthropicToGeminiCliFormat(systemPrompt, messages)
console.log("[GeminiCliHandler] Starting Gemini CLI with prompt length:", prompt.length)
console.log("[GeminiCliHandler] Model:", this.getModel().id)
console.log("[GeminiCliHandler] Path:", this.options.geminiCliPath || "gemini")
const geminiProcess = runGeminiCli({
prompt,
path: this.options.geminiCliPath,
modelId: this.getModel().id,
})
let totalText = ""
let hasYieldedContent = false
for await (const chunk of geminiProcess) {
if (typeof chunk === "string") {
totalText += chunk
hasYieldedContent = true
console.log("[GeminiCliHandler] Received chunk:", chunk.substring(0, 100) + "...")
yield {
type: "text",
text: chunk,
}
} else if (chunk.type === "error") {
console.error("[GeminiCliHandler] Error from Gemini CLI:", chunk.message)
throw new Error(chunk.message)
}
}
// If no content was yielded, something went wrong
if (!hasYieldedContent) {
throw new Error("Gemini CLI did not return any content")
}
// Since Gemini CLI doesn't provide token usage in non-interactive mode,
// we'll estimate based on the text length
const estimatedTokens = Math.ceil(totalText.length / 4)
yield {
type: "usage",
inputTokens: Math.ceil(prompt.length / 4),
outputTokens: estimatedTokens,
cacheReadTokens: 0,
cacheWriteTokens: 0,
totalCost: undefined, // Cost calculation would require actual token counts
}
}
getModel() {
const modelId = this.options.apiModelId
if (modelId && modelId in geminiModels) {
const id = modelId as GeminiModelId
return { id, info: geminiModels[id] }
}
return {
id: geminiDefaultModelId,
info: geminiModels[geminiDefaultModelId],
}
}
}
@@ -21,9 +21,15 @@ export async function updateApiConfigurationProto(
throw new Error("API configuration is required")
}
console.log("[updateApiConfigurationProto] Received request with provider:", request.apiConfiguration.apiProvider)
console.log("[updateApiConfigurationProto] Full proto config:", request.apiConfiguration)
// Convert proto ApiConfiguration to application ApiConfiguration
const appApiConfiguration = convertProtoToApiConfiguration(request.apiConfiguration)
console.log("[updateApiConfigurationProto] Converted to app config with provider:", appApiConfiguration.apiProvider)
console.log("[updateApiConfigurationProto] Full app config:", appApiConfiguration)
// Update the API configuration in storage
await updateApiConfiguration(controller.context, appApiConfiguration)
+2
View File
@@ -10,6 +10,7 @@ export type ApiProvider =
| "ollama"
| "lmstudio"
| "gemini"
| "gemini-cli"
| "openai-native"
| "requesty"
| "together"
@@ -69,6 +70,7 @@ export interface ApiHandlerOptions {
lmStudioBaseUrl?: string
geminiApiKey?: string
geminiBaseUrl?: string
geminiCliPath?: string
openAiNativeApiKey?: string
deepSeekApiKey?: string
requestyApiKey?: string
@@ -202,6 +202,8 @@ function convertApiProviderToProto(provider: string | undefined): ProtoApiProvid
return ProtoApiProvider.LMSTUDIO
case "gemini":
return ProtoApiProvider.GEMINI
case "gemini-cli":
return ProtoApiProvider.GEMINI_CLI
case "openai-native":
return ProtoApiProvider.OPENAI_NATIVE
case "requesty":
@@ -262,6 +264,8 @@ function convertProtoToApiProvider(provider: ProtoApiProvider): ApiProvider {
return "lmstudio"
case ProtoApiProvider.GEMINI:
return "gemini"
case ProtoApiProvider.GEMINI_CLI:
return "gemini-cli"
case ProtoApiProvider.OPENAI_NATIVE:
return "openai-native"
case ProtoApiProvider.REQUESTY:
@@ -379,6 +383,7 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA
sapAiCoreTokenUrl: config.sapAiCoreTokenUrl,
sapAiCoreBaseUrl: config.sapAiCoreBaseUrl,
claudeCodePath: config.claudeCodePath,
geminiCliPath: config.geminiCliPath,
}
}
@@ -458,5 +463,6 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio
sapAiCoreTokenUrl: protoConfig.sapAiCoreTokenUrl,
sapAiCoreBaseUrl: protoConfig.sapAiCoreBaseUrl,
claudeCodePath: protoConfig.claudeCodePath,
geminiCliPath: protoConfig.geminiCliPath,
}
}
+46
View File
@@ -0,0 +1,46 @@
const { spawn } = require("child_process")
// Simple test to see if Gemini CLI outputs tool calls in XML format
async function testGeminiCliTools() {
console.log("Testing Gemini CLI tool call formatting...\n")
const geminiPath = "gemini"
const args = ["--model", "gemini-2.5-flash"]
const child = spawn(geminiPath, args, {
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
env: {
...process.env,
GEMINI_API_KEY: process.env.GEMINI_API_KEY,
},
})
let output = ""
let error = ""
child.stdout.on("data", (data) => {
output += data.toString()
process.stdout.write(data)
})
child.stderr.on("data", (data) => {
error += data.toString()
})
child.on("close", (code) => {
console.log("\n\nProcess exited with code:", code)
if (error) {
console.error("Error output:", error)
}
})
// Send the test prompt
const prompt = "Please read the package.json file to understand this project."
child.stdin.write(prompt)
child.stdin.end()
}
// Run the test
testGeminiCliTools().catch(console.error)
+62
View File
@@ -0,0 +1,62 @@
// Test script for Gemini CLI integration
// This script tests if the Gemini CLI can be invoked programmatically
const { spawn } = require("child_process")
const path = require("path")
// Path to the Gemini CLI
const geminiCliPath = path.join(__dirname, "gemini-cli/packages/cli/dist/index.js")
// Test prompt
const testPrompt = "Hello, can you respond with a simple greeting?"
console.log("Testing Gemini CLI integration...")
console.log("CLI Path:", geminiCliPath)
console.log("Prompt:", testPrompt)
console.log("---")
// Spawn the Gemini CLI process
// When not in TTY mode, the CLI expects input from stdin
const geminiProcess = spawn("node", [geminiCliPath], {
env: {
...process.env,
// Ensure GEMINI_API_KEY is set
GEMINI_API_KEY: process.env.GEMINI_API_KEY,
},
stdio: ["pipe", "pipe", "pipe"], // Allow writing to stdin
})
// Write the prompt to stdin and close it
geminiProcess.stdin.write(testPrompt)
geminiProcess.stdin.end()
// Capture stdout
let output = ""
geminiProcess.stdout.on("data", (data) => {
output += data.toString()
process.stdout.write(data)
})
// Capture stderr
geminiProcess.stderr.on("data", (data) => {
console.error("Error:", data.toString())
})
// Handle process exit
geminiProcess.on("close", (code) => {
console.log("\n---")
console.log(`Process exited with code ${code}`)
if (code === 0 && output.trim()) {
console.log("✅ Test passed! Gemini CLI responded successfully.")
} else {
console.log("❌ Test failed. Check if:")
console.log("1. GEMINI_API_KEY environment variable is set")
console.log("2. The Gemini CLI is built (run npm run build in gemini-cli/packages/cli)")
console.log("3. The CLI has proper permissions")
}
})
geminiProcess.on("error", (err) => {
console.error("Failed to start process:", err)
})
@@ -137,6 +137,8 @@ const ApiOptions = ({
const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => {
const newValue = event.target.value
console.log(`[ApiOptions] handleInputChange - field: ${field}, newValue: ${newValue}`)
// Update local state
setApiConfiguration({
...apiConfiguration,
@@ -145,6 +147,7 @@ const ApiOptions = ({
// If the field is the provider AND saveImmediately is true, save it immediately using the full context state
if (saveImmediately && field === "apiProvider") {
console.log(`[ApiOptions] Saving provider immediately: ${newValue}`)
// Use apiConfiguration from the full extensionState context to send the most complete data
const currentFullApiConfig = extensionState.apiConfiguration
@@ -153,13 +156,14 @@ const ApiOptions = ({
...currentFullApiConfig,
apiProvider: newValue,
}
console.log(`[ApiOptions] Updated config:`, updatedConfig)
const protoConfig = convertApiConfigurationToProto(updatedConfig)
ModelsServiceClient.updateApiConfigurationProto(
UpdateApiConfigurationRequest.create({
apiConfiguration: protoConfig,
}),
).catch((error) => {
console.error("Failed to update API configuration:", error)
console.error("[ApiOptions] Failed to update API configuration:", error)
})
}
}
@@ -274,6 +278,7 @@ const ApiOptions = ({
<VSCodeOption value="openai">OpenAI Compatible</VSCodeOption>
<VSCodeOption value="vertex">GCP Vertex AI</VSCodeOption>
<VSCodeOption value="gemini">Google Gemini</VSCodeOption>
<VSCodeOption value="gemini-cli">Gemini CLI</VSCodeOption>
<VSCodeOption value="deepseek">DeepSeek</VSCodeOption>
<VSCodeOption value="mistral">Mistral</VSCodeOption>
<VSCodeOption value="openai-native">OpenAI</VSCodeOption>
@@ -895,6 +900,36 @@ const ApiOptions = ({
</div>
)}
{selectedProvider === "gemini-cli" && (
<div>
<VSCodeTextField
value={apiConfiguration?.geminiCliPath || ""}
style={{ width: "100%", marginTop: 3 }}
type="text"
onInput={handleInputChange("geminiCliPath")}
placeholder="Default: gemini">
<span style={{ fontWeight: 500 }}>Gemini CLI Path</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
Path to the Gemini CLI executable. The CLI must be installed and configured with authentication.{" "}
<VSCodeLink
href="https://github.com/google/gemini-cli"
style={{
display: "inline",
fontSize: "inherit",
}}>
Learn more about Gemini CLI
</VSCodeLink>
</p>
</div>
)}
{selectedProvider === "requesty" && (
<div>
<VSCodeTextField
@@ -1673,6 +1708,7 @@ const ApiOptions = ({
{selectedProvider === "vertex" &&
createDropdown(apiConfiguration?.vertexRegion === "global" ? vertexGlobalModels : vertexModels)}
{selectedProvider === "gemini" && createDropdown(geminiModels)}
{selectedProvider === "gemini-cli" && createDropdown(geminiModels)}
{selectedProvider === "openai-native" && createDropdown(openAiNativeModels)}
{selectedProvider === "qwen" &&
createDropdown(
@@ -60,6 +60,9 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration):
const provider = apiConfiguration?.apiProvider || "anthropic"
const modelId = apiConfiguration?.apiModelId
console.log(`[normalizeApiConfiguration] provider: ${provider}, modelId: ${modelId}`)
console.log(`[normalizeApiConfiguration] full apiConfiguration:`, apiConfiguration)
const getProviderData = (models: Record<string, ModelInfo>, defaultId: string) => {
let selectedModelId: string
let selectedModelInfo: ModelInfo
@@ -96,6 +99,8 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration):
return getProviderData(vertexModels, vertexDefaultModelId)
case "gemini":
return getProviderData(geminiModels, geminiDefaultModelId)
case "gemini-cli":
return getProviderData(geminiModels, geminiDefaultModelId)
case "openai-native":
return getProviderData(openAiNativeModels, openAiNativeDefaultModelId)
case "deepseek":