Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,5 +72,9 @@
"form-data": ">=4.0.4",
"bluebird": ">=3.7.2"
}
},
"dependencies": {
"@types/node-fetch": "2",
"node-fetch": "2"
}
}
15 changes: 14 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,7 @@
"@roo-code/ipc": "workspace:^",
"@roo-code/telemetry": "workspace:^",
"@roo-code/types": "workspace:^",
"@types/node-fetch": "2",
"@vscode/codicons": "^0.0.36",
"async-mutex": "^0.5.0",
"axios": "^1.12.0",
Expand All @@ -678,14 +679,15 @@
"ignore": "^7.0.3",
"is-wsl": "^3.1.0",
"isbinaryfile": "^5.0.2",
"json5": "^2.2.3",
"jsdom": "^26.0.0",
"json5": "^2.2.3",
"jwt-decode": "^4.0.0",
"lodash.debounce": "^4.0.8",
"lru-cache": "^11.1.0",
"mammoth": "^1.9.1",
"monaco-vscode-textmate-theme-converter": "^0.1.7",
"node-cache": "^5.1.2",
"node-fetch": "2",
"node-ipc": "^12.0.0",
"ollama": "^0.5.17",
"openai": "^5.12.2",
Expand Down Expand Up @@ -729,13 +731,12 @@
"@roo-code/config-eslint": "workspace:^",
"@roo-code/config-typescript": "workspace:^",
"@types/clone-deep": "^4.0.4",
"dotenv": "^16.4.7",
"@types/debug": "^4.1.12",
"@types/diff": "^5.2.3",
"@types/diff-match-patch": "^1.0.36",
"@types/glob": "^8.1.0",
"@types/json5": "^2.2.0",
"@types/jsdom": "^21.1.7",
"@types/json5": "^2.2.0",
"@types/lodash.debounce": "^4.0.9",
"@types/mocha": "^10.0.10",
"@types/node": "20.x",
Expand All @@ -750,6 +751,7 @@
"@types/vscode": "^1.84.0",
"@vscode/test-electron": "^2.5.2",
"@vscode/vsce": "3.3.2",
"dotenv": "^16.4.7",
"esbuild": "^0.25.0",
"execa": "^9.5.2",
"glob": "^11.0.1",
Expand Down
20 changes: 17 additions & 3 deletions src/services/code-index/config-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class CodeIndexConfigManager {
private openAiOptions?: ApiHandlerOptions
private ollamaOptions?: ApiHandlerOptions
private openAiCompatibleOptions?: { baseUrl: string; apiKey: string }
private geminiOptions?: { apiKey: string }
private geminiOptions?: { apiKey: string; baseUrl?: string }
private mistralOptions?: { apiKey: string }
private vercelAiGatewayOptions?: { apiKey: string }
private qdrantUrl?: string = "http://localhost:6333"
Expand Down Expand Up @@ -126,7 +126,14 @@ export class CodeIndexConfigManager {
}
: undefined

this.geminiOptions = geminiApiKey ? { apiKey: geminiApiKey } : undefined
// For Gemini provider, use the generic baseUrl field from codebaseIndexEmbedderBaseUrl
const geminiBaseUrl = codebaseIndexEmbedderProvider === "gemini" ? codebaseIndexEmbedderBaseUrl : ""
this.geminiOptions = geminiApiKey
? {
apiKey: geminiApiKey,
baseUrl: geminiBaseUrl || undefined,
}
: undefined
this.mistralOptions = mistralApiKey ? { apiKey: mistralApiKey } : undefined
this.vercelAiGatewayOptions = vercelAiGatewayApiKey ? { apiKey: vercelAiGatewayApiKey } : undefined
}
Expand All @@ -144,7 +151,7 @@ export class CodeIndexConfigManager {
openAiOptions?: ApiHandlerOptions
ollamaOptions?: ApiHandlerOptions
openAiCompatibleOptions?: { baseUrl: string; apiKey: string }
geminiOptions?: { apiKey: string }
geminiOptions?: { apiKey: string; baseUrl?: string }
mistralOptions?: { apiKey: string }
vercelAiGatewayOptions?: { apiKey: string }
qdrantUrl?: string
Expand All @@ -165,6 +172,7 @@ export class CodeIndexConfigManager {
openAiCompatibleBaseUrl: this.openAiCompatibleOptions?.baseUrl ?? "",
openAiCompatibleApiKey: this.openAiCompatibleOptions?.apiKey ?? "",
geminiApiKey: this.geminiOptions?.apiKey ?? "",
geminiBaseUrl: this.geminiOptions?.baseUrl ?? "",
mistralApiKey: this.mistralOptions?.apiKey ?? "",
vercelAiGatewayApiKey: this.vercelAiGatewayOptions?.apiKey ?? "",
qdrantUrl: this.qdrantUrl ?? "",
Expand Down Expand Up @@ -267,6 +275,7 @@ export class CodeIndexConfigManager {
const prevOpenAiCompatibleApiKey = prev?.openAiCompatibleApiKey ?? ""
const prevModelDimension = prev?.modelDimension
const prevGeminiApiKey = prev?.geminiApiKey ?? ""
const prevGeminiBaseUrl = prev?.geminiBaseUrl ?? ""
const prevMistralApiKey = prev?.mistralApiKey ?? ""
const prevVercelAiGatewayApiKey = prev?.vercelAiGatewayApiKey ?? ""
const prevQdrantUrl = prev?.qdrantUrl ?? ""
Expand Down Expand Up @@ -305,6 +314,7 @@ export class CodeIndexConfigManager {
const currentOpenAiCompatibleApiKey = this.openAiCompatibleOptions?.apiKey ?? ""
const currentModelDimension = this.modelDimension
const currentGeminiApiKey = this.geminiOptions?.apiKey ?? ""
const currentGeminiBaseUrl = this.geminiOptions?.baseUrl ?? ""
const currentMistralApiKey = this.mistralOptions?.apiKey ?? ""
const currentVercelAiGatewayApiKey = this.vercelAiGatewayOptions?.apiKey ?? ""
const currentQdrantUrl = this.qdrantUrl ?? ""
Expand All @@ -329,6 +339,10 @@ export class CodeIndexConfigManager {
return true
}

if (prevGeminiBaseUrl !== currentGeminiBaseUrl) {
return true
}

if (prevMistralApiKey !== currentMistralApiKey) {
return true
}
Expand Down
113 changes: 59 additions & 54 deletions src/services/code-index/embedders/gemini.ts
Original file line number Diff line number Diff line change
@@ -1,91 +1,96 @@
import fetch from "node-fetch"
import { OpenAICompatibleEmbedder } from "./openai-compatible"
import { IEmbedder, EmbeddingResponse, EmbedderInfo } from "../interfaces/embedder"
import { GEMINI_MAX_ITEM_TOKENS } from "../constants"
import { t } from "../../../i18n"
import { TelemetryEventName } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"

/**
* Gemini embedder implementation that wraps the OpenAI Compatible embedder
* with configuration for Google's Gemini embedding API.
*
* Supported models:
* - text-embedding-004 (dimension: 768)
* - gemini-embedding-001 (dimension: 2048)
*/
export class GeminiEmbedder implements IEmbedder {
private readonly openAICompatibleEmbedder: OpenAICompatibleEmbedder
private static readonly GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"
private readonly openAICompatibleEmbedder?: OpenAICompatibleEmbedder
private static readonly DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"
private static readonly DEFAULT_MODEL = "gemini-embedding-001"
private readonly modelId: string
private readonly apiKey: string
private readonly baseUrl: string
private readonly isCustom: boolean

/**
* Creates a new Gemini embedder
* @param apiKey The Gemini API key for authentication
* @param modelId The model ID to use (defaults to gemini-embedding-001)
*/
constructor(apiKey: string, modelId?: string) {
constructor(apiKey: string, modelId?: string, baseUrl?: string) {
if (!apiKey) {
throw new Error(t("embeddings:validation.apiKeyRequired"))
}

// Use provided model or default
this.apiKey = apiKey
this.modelId = modelId || GeminiEmbedder.DEFAULT_MODEL
this.isCustom = !!baseUrl
this.baseUrl = baseUrl || GeminiEmbedder.DEFAULT_BASE_URL

// Create an OpenAI Compatible embedder with Gemini's configuration
this.openAICompatibleEmbedder = new OpenAICompatibleEmbedder(
GeminiEmbedder.GEMINI_BASE_URL,
apiKey,
this.modelId,
GEMINI_MAX_ITEM_TOKENS,
)
if (!this.isCustom) {
this.openAICompatibleEmbedder = new OpenAICompatibleEmbedder(
this.baseUrl,
this.apiKey,
this.modelId,
GEMINI_MAX_ITEM_TOKENS,
)
}
}

/**
* Creates embeddings for the given texts using Gemini's embedding API
* @param texts Array of text strings to embed
* @param model Optional model identifier (uses constructor model if not provided)
* @returns Promise resolving to embedding response
*/
async createEmbeddings(texts: string[], model?: string): Promise<EmbeddingResponse> {
const modelToUse = model || this.modelId

if (!this.isCustom && this.openAICompatibleEmbedder) {
return this.openAICompatibleEmbedder.createEmbeddings(texts, modelToUse)
}

try {
// Use the provided model or fall back to the instance's model
const modelToUse = model || this.modelId
return await this.openAICompatibleEmbedder.createEmbeddings(texts, modelToUse)
const response = await fetch(this.baseUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.apiKey}`,
},
body: JSON.stringify({
input: texts,
model: modelToUse,
}),
})

if (!response.ok) {
const errorBody = await response.text()
console.error(`Gemini custom endpoint error: ${response.status} ${response.statusText}`, errorBody)
throw new Error(`API request failed with status ${response.status}: ${errorBody}`)
}

const data = (await response.json()) as any
return {
embeddings: data.data.map((d: any) => d.embedding),
usage: {
promptTokens: 0,
totalTokens: data.usage.total_tokens,
},
}
} catch (error) {
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
location: "GeminiEmbedder:createEmbeddings",
location: "GeminiEmbedder:createEmbeddings:custom",
})
console.error("Gemini embedder error in createEmbeddings:", error) // kilocode_change
console.error("Gemini custom embedder error in createEmbeddings:", error)
throw error
}
}

/**
* Validates the Gemini embedder configuration by delegating to the underlying OpenAI-compatible embedder
* @returns Promise resolving to validation result with success status and optional error message
*/
async validateConfiguration(): Promise<{ valid: boolean; error?: string }> {
try {
// Delegate validation to the OpenAI-compatible embedder
// The error messages will be specific to Gemini since we're using Gemini's base URL
return await this.openAICompatibleEmbedder.validateConfiguration()
} catch (error) {
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
location: "GeminiEmbedder:validateConfiguration",
})
console.error("Gemini embedder error in validateConfiguration:", error) // kilocode_change
throw error
if (!this.isCustom && this.openAICompatibleEmbedder) {
return this.openAICompatibleEmbedder.validateConfiguration()
}

// For custom URLs, we perform a lazy validation. We assume the URL is valid
// and let any potential errors be caught during the actual embedding process.
// This provides more flexibility for users with custom proxy setups.
return { valid: true }
}

/**
* Returns information about this embedder
*/
get embedderInfo(): EmbedderInfo {
return {
name: "gemini",
Expand Down
3 changes: 2 additions & 1 deletion src/services/code-index/interfaces/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface CodeIndexConfig {
openAiOptions?: ApiHandlerOptions
ollamaOptions?: ApiHandlerOptions
openAiCompatibleOptions?: { baseUrl: string; apiKey: string }
geminiOptions?: { apiKey: string }
geminiOptions?: { apiKey: string; baseUrl?: string }
mistralOptions?: { apiKey: string }
vercelAiGatewayOptions?: { apiKey: string }
qdrantUrl?: string
Expand All @@ -35,6 +35,7 @@ export type PreviousConfigSnapshot = {
openAiCompatibleBaseUrl?: string
openAiCompatibleApiKey?: string
geminiApiKey?: string
geminiBaseUrl?: string
mistralApiKey?: string
vercelAiGatewayApiKey?: string
qdrantUrl?: string
Expand Down
2 changes: 1 addition & 1 deletion src/services/code-index/service-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class CodeIndexServiceFactory {
if (!config.geminiOptions?.apiKey) {
throw new Error(t("embeddings:serviceFactory.geminiConfigMissing"))
}
return new GeminiEmbedder(config.geminiOptions.apiKey, config.modelId)
return new GeminiEmbedder(config.geminiOptions.apiKey, config.modelId, config.geminiOptions.baseUrl)
} else if (provider === "mistral") {
if (!config.mistralOptions?.apiKey) {
throw new Error(t("embeddings:serviceFactory.mistralConfigMissing"))
Expand Down
Loading