mirror of
https://github.com/cline/cline.git
synced 2026-09-06 12:28:08 +08:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bac990e5ad | |||
| a694fa27eb | |||
| 9edbccc54f |
+2
-1
@@ -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
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user