diff --git a/packages/types/src/codebase-index.ts b/packages/types/src/codebase-index.ts index e86c17627f..eea8a069a4 100644 --- a/packages/types/src/codebase-index.ts +++ b/packages/types/src/codebase-index.ts @@ -7,7 +7,7 @@ import { z } from "zod" export const codebaseIndexConfigSchema = z.object({ codebaseIndexEnabled: z.boolean().optional(), codebaseIndexQdrantUrl: z.string().optional(), - codebaseIndexEmbedderProvider: z.enum(["openai", "ollama", "openai-compatible"]).optional(), + codebaseIndexEmbedderProvider: z.enum(["openai", "ollama", "openai-compatible", "gemini"]).optional(), codebaseIndexEmbedderBaseUrl: z.string().optional(), codebaseIndexEmbedderModelId: z.string().optional(), }) @@ -22,6 +22,7 @@ export const codebaseIndexModelsSchema = z.object({ openai: z.record(z.string(), z.object({ dimension: z.number() })).optional(), ollama: z.record(z.string(), z.object({ dimension: z.number() })).optional(), "openai-compatible": z.record(z.string(), z.object({ dimension: z.number() })).optional(), + gemini: z.record(z.string(), z.object({ dimension: z.number() })).optional(), }) export type CodebaseIndexModels = z.infer @@ -36,6 +37,7 @@ export const codebaseIndexProviderSchema = z.object({ codebaseIndexOpenAiCompatibleBaseUrl: z.string().optional(), codebaseIndexOpenAiCompatibleApiKey: z.string().optional(), codebaseIndexOpenAiCompatibleModelDimension: z.number().optional(), + codebaseIndexGeminiApiKey: z.string().optional(), }) export type CodebaseIndexProvider = z.infer diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index e713cafa4c..b228fe0787 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -143,6 +143,7 @@ export const SECRET_STATE_KEYS = [ "codeIndexOpenAiKey", "codeIndexQdrantApiKey", "codebaseIndexOpenAiCompatibleApiKey", + "codebaseIndexGeminiApiKey", ] as const satisfies readonly (keyof ProviderSettings)[] export type SecretState = Pick diff --git a/src/services/code-index/__tests__/config-manager.spec.ts b/src/services/code-index/__tests__/config-manager.spec.ts index f5a759c158..58d213202c 100644 --- a/src/services/code-index/__tests__/config-manager.spec.ts +++ b/src/services/code-index/__tests__/config-manager.spec.ts @@ -902,6 +902,46 @@ describe("CodeIndexConfigManager", () => { expect(configManager.isFeatureConfigured).toBe(false) }) + it("should validate Gemini configuration correctly", async () => { + mockContextProxy.getGlobalState.mockImplementation((key: string) => { + if (key === "codebaseIndexConfig") { + return { + codebaseIndexEnabled: true, + codebaseIndexQdrantUrl: "http://qdrant.local", + codebaseIndexEmbedderProvider: "gemini", + } + } + return undefined + }) + mockContextProxy.getSecret.mockImplementation((key: string) => { + if (key === "codebaseIndexGeminiApiKey") return "test-gemini-key" + return undefined + }) + + await configManager.loadConfiguration() + expect(configManager.isFeatureConfigured).toBe(true) + }) + + it("should return false when Gemini API key is missing", async () => { + mockContextProxy.getGlobalState.mockImplementation((key: string) => { + if (key === "codebaseIndexConfig") { + return { + codebaseIndexEnabled: true, + codebaseIndexQdrantUrl: "http://qdrant.local", + codebaseIndexEmbedderProvider: "gemini", + } + } + return undefined + }) + mockContextProxy.getSecret.mockImplementation((key: string) => { + if (key === "codebaseIndexGeminiApiKey") return "" + return undefined + }) + + await configManager.loadConfiguration() + expect(configManager.isFeatureConfigured).toBe(false) + }) + it("should return false when required values are missing", async () => { mockContextProxy.getGlobalState.mockReturnValue({ codebaseIndexEnabled: true, diff --git a/src/services/code-index/__tests__/service-factory.spec.ts b/src/services/code-index/__tests__/service-factory.spec.ts index a539549bad..5e2d878ffb 100644 --- a/src/services/code-index/__tests__/service-factory.spec.ts +++ b/src/services/code-index/__tests__/service-factory.spec.ts @@ -3,12 +3,14 @@ import { CodeIndexServiceFactory } from "../service-factory" import { OpenAiEmbedder } from "../embedders/openai" import { CodeIndexOllamaEmbedder } from "../embedders/ollama" import { OpenAICompatibleEmbedder } from "../embedders/openai-compatible" +import { GeminiEmbedder } from "../embedders/gemini" import { QdrantVectorStore } from "../vector-store/qdrant-client" // Mock the embedders and vector store vitest.mock("../embedders/openai") vitest.mock("../embedders/ollama") vitest.mock("../embedders/openai-compatible") +vitest.mock("../embedders/gemini") vitest.mock("../vector-store/qdrant-client") // Mock the embedding models module @@ -20,6 +22,7 @@ vitest.mock("../../../shared/embeddingModels", () => ({ const MockedOpenAiEmbedder = OpenAiEmbedder as MockedClass const MockedCodeIndexOllamaEmbedder = CodeIndexOllamaEmbedder as MockedClass const MockedOpenAICompatibleEmbedder = OpenAICompatibleEmbedder as MockedClass +const MockedGeminiEmbedder = GeminiEmbedder as MockedClass const MockedQdrantVectorStore = QdrantVectorStore as MockedClass // Import the mocked functions @@ -259,6 +262,49 @@ describe("CodeIndexServiceFactory", () => { ) }) + it("should create GeminiEmbedder when using Gemini provider", () => { + // Arrange + const testConfig = { + embedderProvider: "gemini", + geminiOptions: { + apiKey: "test-gemini-api-key", + }, + } + mockConfigManager.getConfig.mockReturnValue(testConfig as any) + + // Act + factory.createEmbedder() + + // Assert + expect(MockedGeminiEmbedder).toHaveBeenCalledWith("test-gemini-api-key") + }) + + it("should throw error when Gemini API key is missing", () => { + // Arrange + const testConfig = { + embedderProvider: "gemini", + geminiOptions: { + apiKey: undefined, + }, + } + mockConfigManager.getConfig.mockReturnValue(testConfig as any) + + // Act & Assert + expect(() => factory.createEmbedder()).toThrow("Gemini configuration missing for embedder creation") + }) + + it("should throw error when Gemini options are missing", () => { + // Arrange + const testConfig = { + embedderProvider: "gemini", + geminiOptions: undefined, + } + mockConfigManager.getConfig.mockReturnValue(testConfig as any) + + // Act & Assert + expect(() => factory.createEmbedder()).toThrow("Gemini configuration missing for embedder creation") + }) + it("should throw error for invalid embedder provider", () => { // Arrange const testConfig = { @@ -454,6 +500,30 @@ describe("CodeIndexServiceFactory", () => { ) }) + it("should use fixed dimension 768 for Gemini provider", () => { + // Arrange + const testConfig = { + embedderProvider: "gemini", + modelId: "text-embedding-004", // This is ignored by Gemini + qdrantUrl: "http://localhost:6333", + qdrantApiKey: "test-key", + } + mockConfigManager.getConfig.mockReturnValue(testConfig as any) + + // Act + factory.createVectorStore() + + // Assert + // getModelDimension should not be called for Gemini + expect(mockGetModelDimension).not.toHaveBeenCalled() + expect(MockedQdrantVectorStore).toHaveBeenCalledWith( + "/test/workspace", + "http://localhost:6333", + 768, // Fixed dimension for Gemini + "test-key", + ) + }) + it("should use default model when config.modelId is undefined", () => { // Arrange const testConfig = { diff --git a/src/services/code-index/config-manager.ts b/src/services/code-index/config-manager.ts index 678cec36a1..104c4fc261 100644 --- a/src/services/code-index/config-manager.ts +++ b/src/services/code-index/config-manager.ts @@ -16,6 +16,7 @@ export class CodeIndexConfigManager { private openAiOptions?: ApiHandlerOptions private ollamaOptions?: ApiHandlerOptions private openAiCompatibleOptions?: { baseUrl: string; apiKey: string; modelDimension?: number } + private geminiOptions?: { apiKey: string } private qdrantUrl?: string = "http://localhost:6333" private qdrantApiKey?: string private searchMinScore?: number @@ -55,6 +56,7 @@ export class CodeIndexConfigManager { const openAiCompatibleModelDimension = this.contextProxy?.getGlobalState( "codebaseIndexOpenAiCompatibleModelDimension", ) as number | undefined + const geminiApiKey = this.contextProxy?.getSecret("codebaseIndexGeminiApiKey") ?? "" // Update instance variables with configuration this.isEnabled = codebaseIndexEnabled || false @@ -68,6 +70,8 @@ export class CodeIndexConfigManager { this.embedderProvider = "ollama" } else if (codebaseIndexEmbedderProvider === "openai-compatible") { this.embedderProvider = "openai-compatible" + } else if (codebaseIndexEmbedderProvider === "gemini") { + this.embedderProvider = "gemini" } else { this.embedderProvider = "openai" } @@ -86,6 +90,8 @@ export class CodeIndexConfigManager { modelDimension: openAiCompatibleModelDimension, } : undefined + + this.geminiOptions = geminiApiKey ? { apiKey: geminiApiKey } : undefined } /** @@ -101,6 +107,7 @@ export class CodeIndexConfigManager { openAiOptions?: ApiHandlerOptions ollamaOptions?: ApiHandlerOptions openAiCompatibleOptions?: { baseUrl: string; apiKey: string } + geminiOptions?: { apiKey: string } qdrantUrl?: string qdrantApiKey?: string searchMinScore?: number @@ -118,6 +125,7 @@ export class CodeIndexConfigManager { openAiCompatibleBaseUrl: this.openAiCompatibleOptions?.baseUrl ?? "", openAiCompatibleApiKey: this.openAiCompatibleOptions?.apiKey ?? "", openAiCompatibleModelDimension: this.openAiCompatibleOptions?.modelDimension, + geminiApiKey: this.geminiOptions?.apiKey ?? "", qdrantUrl: this.qdrantUrl ?? "", qdrantApiKey: this.qdrantApiKey ?? "", } @@ -137,6 +145,7 @@ export class CodeIndexConfigManager { openAiOptions: this.openAiOptions, ollamaOptions: this.ollamaOptions, openAiCompatibleOptions: this.openAiCompatibleOptions, + geminiOptions: this.geminiOptions, qdrantUrl: this.qdrantUrl, qdrantApiKey: this.qdrantApiKey, searchMinScore: this.searchMinScore, @@ -165,6 +174,10 @@ export class CodeIndexConfigManager { const apiKey = this.openAiCompatibleOptions?.apiKey const qdrantUrl = this.qdrantUrl return !!(baseUrl && apiKey && qdrantUrl) + } else if (this.embedderProvider === "gemini") { + const apiKey = this.geminiOptions?.apiKey + const qdrantUrl = this.qdrantUrl + return !!(apiKey && qdrantUrl) } return false // Should not happen if embedderProvider is always set correctly } @@ -185,6 +198,7 @@ export class CodeIndexConfigManager { const prevOpenAiCompatibleBaseUrl = prev?.openAiCompatibleBaseUrl ?? "" const prevOpenAiCompatibleApiKey = prev?.openAiCompatibleApiKey ?? "" const prevOpenAiCompatibleModelDimension = prev?.openAiCompatibleModelDimension + const prevGeminiApiKey = prev?.geminiApiKey ?? "" const prevQdrantUrl = prev?.qdrantUrl ?? "" const prevQdrantApiKey = prev?.qdrantApiKey ?? "" @@ -242,6 +256,13 @@ export class CodeIndexConfigManager { } } + if (this.embedderProvider === "gemini") { + const currentGeminiApiKey = this.geminiOptions?.apiKey ?? "" + if (prevGeminiApiKey !== currentGeminiApiKey) { + return true + } + } + // Qdrant configuration changes const currentQdrantUrl = this.qdrantUrl ?? "" const currentQdrantApiKey = this.qdrantApiKey ?? "" @@ -292,6 +313,7 @@ export class CodeIndexConfigManager { openAiOptions: this.openAiOptions, ollamaOptions: this.ollamaOptions, openAiCompatibleOptions: this.openAiCompatibleOptions, + geminiOptions: this.geminiOptions, qdrantUrl: this.qdrantUrl, qdrantApiKey: this.qdrantApiKey, searchMinScore: this.searchMinScore, diff --git a/src/services/code-index/constants/index.ts b/src/services/code-index/constants/index.ts index cbf6941817..fc66571fbb 100644 --- a/src/services/code-index/constants/index.ts +++ b/src/services/code-index/constants/index.ts @@ -23,3 +23,6 @@ export const PARSING_CONCURRENCY = 10 export const MAX_BATCH_TOKENS = 100000 export const MAX_ITEM_TOKENS = 8191 export const BATCH_PROCESSING_CONCURRENCY = 10 + +/**Gemini Embedder */ +export const GEMINI_MAX_ITEM_TOKENS = 2048 diff --git a/src/services/code-index/embedders/__tests__/gemini.spec.ts b/src/services/code-index/embedders/__tests__/gemini.spec.ts new file mode 100644 index 0000000000..6d518f65aa --- /dev/null +++ b/src/services/code-index/embedders/__tests__/gemini.spec.ts @@ -0,0 +1,58 @@ +import { vitest, describe, it, expect, beforeEach } from "vitest" +import type { MockedClass } from "vitest" +import { GeminiEmbedder } from "../gemini" +import { OpenAICompatibleEmbedder } from "../openai-compatible" + +// Mock the OpenAICompatibleEmbedder +vitest.mock("../openai-compatible") + +const MockedOpenAICompatibleEmbedder = OpenAICompatibleEmbedder as MockedClass + +describe("GeminiEmbedder", () => { + let embedder: GeminiEmbedder + + beforeEach(() => { + vitest.clearAllMocks() + }) + + describe("constructor", () => { + it("should create an instance with correct fixed values passed to OpenAICompatibleEmbedder", () => { + // Arrange + const apiKey = "test-gemini-api-key" + + // Act + embedder = new GeminiEmbedder(apiKey) + + // Assert + expect(MockedOpenAICompatibleEmbedder).toHaveBeenCalledWith( + "https://generativelanguage.googleapis.com/v1beta/openai/", + apiKey, + "text-embedding-004", + 2048, + ) + }) + + it("should throw error when API key is not provided", () => { + // Act & Assert + expect(() => new GeminiEmbedder("")).toThrow("API key is required for Gemini embedder") + expect(() => new GeminiEmbedder(null as any)).toThrow("API key is required for Gemini embedder") + expect(() => new GeminiEmbedder(undefined as any)).toThrow("API key is required for Gemini embedder") + }) + }) + + describe("embedderInfo", () => { + it("should return correct embedder info with dimension 768", () => { + // Arrange + embedder = new GeminiEmbedder("test-api-key") + + // Act + const info = embedder.embedderInfo + + // Assert + expect(info).toEqual({ + name: "gemini", + }) + expect(GeminiEmbedder.dimension).toBe(768) + }) + }) +}) diff --git a/src/services/code-index/embedders/gemini.ts b/src/services/code-index/embedders/gemini.ts new file mode 100644 index 0000000000..38bb132df7 --- /dev/null +++ b/src/services/code-index/embedders/gemini.ts @@ -0,0 +1,64 @@ +import { OpenAICompatibleEmbedder } from "./openai-compatible" +import { IEmbedder, EmbeddingResponse, EmbedderInfo } from "../interfaces/embedder" +import { GEMINI_MAX_ITEM_TOKENS } from "../constants" + +/** + * Gemini embedder implementation that wraps the OpenAI Compatible embedder + * with fixed configuration for Google's Gemini embedding API. + * + * Fixed values: + * - Base URL: https://generativelanguage.googleapis.com/v1beta/openai/ + * - Model: text-embedding-004 + * - Dimension: 768 + */ +export class GeminiEmbedder implements IEmbedder { + private readonly openAICompatibleEmbedder: OpenAICompatibleEmbedder + private static readonly GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/" + private static readonly GEMINI_MODEL = "text-embedding-004" + private static readonly GEMINI_DIMENSION = 768 + + /** + * Creates a new Gemini embedder + * @param apiKey The Gemini API key for authentication + */ + constructor(apiKey: string) { + if (!apiKey) { + throw new Error("API key is required for Gemini embedder") + } + + // Create an OpenAI Compatible embedder with Gemini's fixed configuration + this.openAICompatibleEmbedder = new OpenAICompatibleEmbedder( + GeminiEmbedder.GEMINI_BASE_URL, + apiKey, + GeminiEmbedder.GEMINI_MODEL, + 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 (ignored - always uses text-embedding-004) + * @returns Promise resolving to embedding response + */ + async createEmbeddings(texts: string[], model?: string): Promise { + // Always use the fixed Gemini model, ignoring any passed model parameter + return this.openAICompatibleEmbedder.createEmbeddings(texts, GeminiEmbedder.GEMINI_MODEL) + } + + /** + * Returns information about this embedder + */ + get embedderInfo(): EmbedderInfo { + return { + name: "gemini", + } + } + + /** + * Gets the fixed dimension for Gemini embeddings + */ + static get dimension(): number { + return GeminiEmbedder.GEMINI_DIMENSION + } +} diff --git a/src/services/code-index/embedders/openai-compatible.ts b/src/services/code-index/embedders/openai-compatible.ts index 0983cc297f..77b82af67a 100644 --- a/src/services/code-index/embedders/openai-compatible.ts +++ b/src/services/code-index/embedders/openai-compatible.ts @@ -29,14 +29,16 @@ interface OpenAIEmbeddingResponse { export class OpenAICompatibleEmbedder implements IEmbedder { private embeddingsClient: OpenAI private readonly defaultModelId: string + private readonly maxItemTokens: number /** * Creates a new OpenAI Compatible embedder * @param baseUrl The base URL for the OpenAI-compatible API endpoint * @param apiKey The API key for authentication * @param modelId Optional model identifier (defaults to "text-embedding-3-small") + * @param maxItemTokens Optional maximum tokens per item (defaults to MAX_ITEM_TOKENS) */ - constructor(baseUrl: string, apiKey: string, modelId?: string) { + constructor(baseUrl: string, apiKey: string, modelId?: string, maxItemTokens?: number) { if (!baseUrl) { throw new Error("Base URL is required for OpenAI Compatible embedder") } @@ -49,6 +51,7 @@ export class OpenAICompatibleEmbedder implements IEmbedder { apiKey: apiKey, }) this.defaultModelId = modelId || getDefaultModelId("openai-compatible") + this.maxItemTokens = maxItemTokens || MAX_ITEM_TOKENS } /** @@ -72,12 +75,12 @@ export class OpenAICompatibleEmbedder implements IEmbedder { const text = remainingTexts[i] const itemTokens = Math.ceil(text.length / 4) - if (itemTokens > MAX_ITEM_TOKENS) { + if (itemTokens > this.maxItemTokens) { console.warn( t("embeddings:textExceedsTokenLimit", { index: i, itemTokens, - maxTokens: MAX_ITEM_TOKENS, + maxTokens: this.maxItemTokens, }), ) processedIndices.push(i) diff --git a/src/services/code-index/interfaces/config.ts b/src/services/code-index/interfaces/config.ts index 0843120fd9..3501ec950c 100644 --- a/src/services/code-index/interfaces/config.ts +++ b/src/services/code-index/interfaces/config.ts @@ -12,6 +12,7 @@ export interface CodeIndexConfig { openAiOptions?: ApiHandlerOptions ollamaOptions?: ApiHandlerOptions openAiCompatibleOptions?: { baseUrl: string; apiKey: string; modelDimension?: number } + geminiOptions?: { apiKey: string } qdrantUrl?: string qdrantApiKey?: string searchMinScore?: number @@ -30,6 +31,7 @@ export type PreviousConfigSnapshot = { openAiCompatibleBaseUrl?: string openAiCompatibleApiKey?: string openAiCompatibleModelDimension?: number + geminiApiKey?: string qdrantUrl?: string qdrantApiKey?: string } diff --git a/src/services/code-index/interfaces/embedder.ts b/src/services/code-index/interfaces/embedder.ts index 820fba9b8e..3ea6293aa5 100644 --- a/src/services/code-index/interfaces/embedder.ts +++ b/src/services/code-index/interfaces/embedder.ts @@ -21,7 +21,7 @@ export interface EmbeddingResponse { } } -export type AvailableEmbedders = "openai" | "ollama" | "openai-compatible" +export type AvailableEmbedders = "openai" | "ollama" | "openai-compatible" | "gemini" export interface EmbedderInfo { name: AvailableEmbedders diff --git a/src/services/code-index/interfaces/manager.ts b/src/services/code-index/interfaces/manager.ts index f3d577d82f..70e3fd9765 100644 --- a/src/services/code-index/interfaces/manager.ts +++ b/src/services/code-index/interfaces/manager.ts @@ -70,7 +70,7 @@ export interface ICodeIndexManager { } export type IndexingState = "Standby" | "Indexing" | "Indexed" | "Error" -export type EmbedderProvider = "openai" | "ollama" | "openai-compatible" +export type EmbedderProvider = "openai" | "ollama" | "openai-compatible" | "gemini" export interface IndexProgressUpdate { systemStatus: IndexingState diff --git a/src/services/code-index/service-factory.ts b/src/services/code-index/service-factory.ts index afd083b204..2a19c8ebab 100644 --- a/src/services/code-index/service-factory.ts +++ b/src/services/code-index/service-factory.ts @@ -2,6 +2,7 @@ import * as vscode from "vscode" import { OpenAiEmbedder } from "./embedders/openai" import { CodeIndexOllamaEmbedder } from "./embedders/ollama" import { OpenAICompatibleEmbedder } from "./embedders/openai-compatible" +import { GeminiEmbedder } from "./embedders/gemini" import { EmbedderProvider, getDefaultModelId, getModelDimension } from "../../shared/embeddingModels" import { QdrantVectorStore } from "./vector-store/qdrant-client" import { codeParser, DirectoryScanner, FileWatcher } from "./processors" @@ -53,6 +54,11 @@ export class CodeIndexServiceFactory { config.openAiCompatibleOptions.apiKey, config.modelId, ) + } else if (provider === "gemini") { + if (!config.geminiOptions?.apiKey) { + throw new Error("Gemini configuration missing for embedder creation") + } + return new GeminiEmbedder(config.geminiOptions.apiKey) } throw new Error(`Invalid embedder type configured: ${config.embedderProvider}`) @@ -78,6 +84,9 @@ export class CodeIndexServiceFactory { // Fallback if not provided or invalid in openAiCompatibleOptions vectorSize = getModelDimension(provider, modelId) } + } else if (provider === "gemini") { + // Gemini's text-embedding-004 has a fixed dimension of 768 + vectorSize = 768 } else { vectorSize = getModelDimension(provider, modelId) } diff --git a/src/shared/embeddingModels.ts b/src/shared/embeddingModels.ts index cd7c1d4e6b..518cd0a341 100644 --- a/src/shared/embeddingModels.ts +++ b/src/shared/embeddingModels.ts @@ -2,7 +2,7 @@ * Defines profiles for different embedding models, including their dimensions. */ -export type EmbedderProvider = "openai" | "ollama" | "openai-compatible" // Add other providers as needed +export type EmbedderProvider = "openai" | "ollama" | "openai-compatible" | "gemini" // Add other providers as needed export interface EmbeddingModelProfile { dimension: number @@ -34,6 +34,9 @@ export const EMBEDDING_MODEL_PROFILES: EmbeddingModelProfiles = { "text-embedding-3-large": { dimension: 3072 }, "text-embedding-ada-002": { dimension: 1536 }, }, + gemini: { + "text-embedding-004": { dimension: 768 }, + }, } /** @@ -85,6 +88,10 @@ export function getDefaultModelId(provider: EmbedderProvider): string { // Return a placeholder or throw an error, depending on desired behavior return "unknown-default" // Placeholder specific model ID } + + case "gemini": + return "text-embedding-004" + default: // Fallback for unknown providers console.warn(`Unknown provider for default model ID: ${provider}. Falling back to OpenAI default.`) diff --git a/webview-ui/src/components/settings/CodeIndexSettings.tsx b/webview-ui/src/components/settings/CodeIndexSettings.tsx index 13c9524d9f..39464337c0 100644 --- a/webview-ui/src/components/settings/CodeIndexSettings.tsx +++ b/webview-ui/src/components/settings/CodeIndexSettings.tsx @@ -51,6 +51,7 @@ export const CodeIndexSettings: React.FC = ({ areSettingsCommitted, }) => { const { t } = useAppTranslation() + const DEFAULT_QDRANT_URL = "http://localhost:6333" const [indexingStatus, setIndexingStatus] = useState({ systemStatus: "Standby", message: "", @@ -62,9 +63,9 @@ export const CodeIndexSettings: React.FC = ({ // Safely calculate available models for current provider const currentProvider = codebaseIndexConfig?.codebaseIndexEmbedderProvider const modelsForProvider = - currentProvider === "openai" || currentProvider === "ollama" || currentProvider === "openai-compatible" - ? codebaseIndexModels?.[currentProvider] || codebaseIndexModels?.openai - : codebaseIndexModels?.openai + currentProvider === "openai" || currentProvider === "openai-compatible" + ? (codebaseIndexModels?.openai ?? codebaseIndexModels?.["openai-compatible"]) + : codebaseIndexModels?.[currentProvider as keyof typeof codebaseIndexModels] const availableModelIds = Object.keys(modelsForProvider || {}) useEffect(() => { @@ -145,6 +146,10 @@ export const CodeIndexSettings: React.FC = ({ .positive("Dimension must be a positive number") .optional(), }), + gemini: baseSchema.extend({ + codebaseIndexEmbedderProvider: z.literal("gemini"), + codebaseIndexGeminiApiKey: z.string().min(1, "Gemini API key is required"), + }), } try { @@ -153,7 +158,9 @@ export const CodeIndexSettings: React.FC = ({ ? providerSchemas.openai : config.codebaseIndexEmbedderProvider === "ollama" ? providerSchemas.ollama - : providerSchemas["openai-compatible"] + : config.codebaseIndexEmbedderProvider === "gemini" + ? providerSchemas.gemini + : providerSchemas["openai-compatible"] schema.parse({ ...config, @@ -161,6 +168,7 @@ export const CodeIndexSettings: React.FC = ({ codebaseIndexOpenAiCompatibleBaseUrl: apiConfig.codebaseIndexOpenAiCompatibleBaseUrl, codebaseIndexOpenAiCompatibleApiKey: apiConfig.codebaseIndexOpenAiCompatibleApiKey, codebaseIndexOpenAiCompatibleModelDimension: apiConfig.codebaseIndexOpenAiCompatibleModelDimension, + codebaseIndexGeminiApiKey: apiConfig.codebaseIndexGeminiApiKey, }) return true } catch { @@ -275,6 +283,7 @@ export const CodeIndexSettings: React.FC = ({ {t("settings:codeIndex.openaiCompatibleProvider")} + {t("settings:codeIndex.geminiProvider")} @@ -419,19 +428,47 @@ export const CodeIndexSettings: React.FC = ({ )} + {codebaseIndexConfig?.codebaseIndexEmbedderProvider === "gemini" && ( +
+
+
{t("settings:codeIndex.geminiApiKeyLabel")}
+
+
+ + setApiConfigurationField("codebaseIndexGeminiApiKey", e.target.value) + } + placeholder={t("settings:codeIndex.geminiApiKeyPlaceholder")} + style={{ width: "100%" }}> +
+
+ )} +
{t("settings:codeIndex.qdrantUrlLabel")}
setCachedStateField("codebaseIndexConfig", { ...codebaseIndexConfig, codebaseIndexQdrantUrl: e.target.value, }) } + onBlur={(e: any) => { + // Set default value if field is empty on blur + if (!e.target.value) { + setCachedStateField("codebaseIndexConfig", { + ...codebaseIndexConfig, + codebaseIndexQdrantUrl: DEFAULT_QDRANT_URL, + }) + } + }} style={{ width: "100%" }}>
diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index ea4f974149..07efdcb251 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -44,6 +44,9 @@ "selectProviderPlaceholder": "Seleccionar proveïdor", "openaiProvider": "OpenAI", "ollamaProvider": "Ollama", + "geminiProvider": "Gemini", + "geminiApiKeyLabel": "Clau API:", + "geminiApiKeyPlaceholder": "Introduïu la vostra clau d'API de Gemini", "openaiCompatibleProvider": "Compatible amb OpenAI", "openaiCompatibleBaseUrlLabel": "URL base:", "openaiCompatibleApiKeyLabel": "Clau API:", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index c5e161bafe..77d574c0d2 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -44,6 +44,9 @@ "selectProviderPlaceholder": "Anbieter auswählen", "openaiProvider": "OpenAI", "ollamaProvider": "Ollama", + "geminiProvider": "Gemini", + "geminiApiKeyLabel": "API-Schlüssel:", + "geminiApiKeyPlaceholder": "Geben Sie Ihren Gemini-API-Schlüssel ein", "openaiCompatibleProvider": "OpenAI-kompatibel", "openaiCompatibleBaseUrlLabel": "Basis-URL:", "openaiCompatibleApiKeyLabel": "API-Schlüssel:", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 14981ea499..81efa9c3d0 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -44,6 +44,9 @@ "selectProviderPlaceholder": "Select provider", "openaiProvider": "OpenAI", "ollamaProvider": "Ollama", + "geminiProvider": "Gemini", + "geminiApiKeyLabel": "API Key:", + "geminiApiKeyPlaceholder": "Enter your Gemini API key", "openaiCompatibleProvider": "OpenAI Compatible", "openaiKeyLabel": "OpenAI Key:", "openaiCompatibleBaseUrlLabel": "Base URL:", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 3e3d20cce1..af40f85deb 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -44,6 +44,9 @@ "selectProviderPlaceholder": "Seleccionar proveedor", "openaiProvider": "OpenAI", "ollamaProvider": "Ollama", + "geminiProvider": "Gemini", + "geminiApiKeyLabel": "Clave API:", + "geminiApiKeyPlaceholder": "Introduce tu clave de API de Gemini", "openaiCompatibleProvider": "Compatible con OpenAI", "openaiCompatibleBaseUrlLabel": "URL base:", "openaiCompatibleApiKeyLabel": "Clave API:", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 5635251876..654207c4ce 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -44,6 +44,9 @@ "selectProviderPlaceholder": "Sélectionner un fournisseur", "openaiProvider": "OpenAI", "ollamaProvider": "Ollama", + "geminiProvider": "Gemini", + "geminiApiKeyLabel": "Clé API :", + "geminiApiKeyPlaceholder": "Entrez votre clé API Gemini", "openaiCompatibleProvider": "Compatible OpenAI", "openaiCompatibleBaseUrlLabel": "URL de base :", "openaiCompatibleApiKeyLabel": "Clé API :", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 8de5bd1a19..0d15cfb939 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -44,6 +44,9 @@ "selectProviderPlaceholder": "प्रदाता चुनें", "openaiProvider": "OpenAI", "ollamaProvider": "Ollama", + "geminiProvider": "Gemini", + "geminiApiKeyLabel": "API कुंजी:", + "geminiApiKeyPlaceholder": "अपना जेमिनी एपीआई कुंजी दर्ज करें", "openaiCompatibleProvider": "OpenAI संगत", "openaiCompatibleBaseUrlLabel": "आधार URL:", "openaiCompatibleApiKeyLabel": "API कुंजी:", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 8def303c05..49b077d7af 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -44,6 +44,9 @@ "selectProviderPlaceholder": "Pilih provider", "openaiProvider": "OpenAI", "ollamaProvider": "Ollama", + "geminiProvider": "Gemini", + "geminiApiKeyLabel": "API Key:", + "geminiApiKeyPlaceholder": "Masukkan kunci API Gemini Anda", "openaiCompatibleProvider": "OpenAI Compatible", "openaiKeyLabel": "OpenAI Key:", "openaiCompatibleBaseUrlLabel": "Base URL:", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 572f99cbb0..3060dace1b 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -44,6 +44,9 @@ "selectProviderPlaceholder": "Seleziona fornitore", "openaiProvider": "OpenAI", "ollamaProvider": "Ollama", + "geminiProvider": "Gemini", + "geminiApiKeyLabel": "Chiave API:", + "geminiApiKeyPlaceholder": "Inserisci la tua chiave API Gemini", "openaiCompatibleProvider": "Compatibile con OpenAI", "openaiCompatibleBaseUrlLabel": "URL di base:", "openaiCompatibleApiKeyLabel": "Chiave API:", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index c6a681e549..093c975365 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -44,6 +44,9 @@ "selectProviderPlaceholder": "プロバイダーを選択", "openaiProvider": "OpenAI", "ollamaProvider": "Ollama", + "geminiProvider": "Gemini", + "geminiApiKeyLabel": "APIキー:", + "geminiApiKeyPlaceholder": "Gemini APIキーを入力してください", "openaiCompatibleProvider": "OpenAI互換", "openaiCompatibleBaseUrlLabel": "ベースURL:", "openaiCompatibleApiKeyLabel": "APIキー:", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 6ae68428bd..82f6387a8b 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -44,6 +44,9 @@ "selectProviderPlaceholder": "제공자 선택", "openaiProvider": "OpenAI", "ollamaProvider": "Ollama", + "geminiProvider": "Gemini", + "geminiApiKeyLabel": "API 키:", + "geminiApiKeyPlaceholder": "Gemini API 키를 입력하세요", "openaiCompatibleProvider": "OpenAI 호환", "openaiCompatibleBaseUrlLabel": "기본 URL:", "openaiCompatibleApiKeyLabel": "API 키:", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index c6d1bb7992..38805f5815 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -44,6 +44,9 @@ "selectProviderPlaceholder": "Selecteer provider", "openaiProvider": "OpenAI", "ollamaProvider": "Ollama", + "geminiProvider": "Gemini", + "geminiApiKeyLabel": "API-sleutel:", + "geminiApiKeyPlaceholder": "Voer uw Gemini API-sleutel in", "openaiCompatibleProvider": "OpenAI-compatibel", "openaiCompatibleBaseUrlLabel": "Basis-URL:", "openaiCompatibleApiKeyLabel": "API-sleutel:", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index b702d7b5a5..2e3898f844 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -44,6 +44,9 @@ "selectProviderPlaceholder": "Wybierz dostawcę", "openaiProvider": "OpenAI", "ollamaProvider": "Ollama", + "geminiProvider": "Gemini", + "geminiApiKeyLabel": "Klucz API:", + "geminiApiKeyPlaceholder": "Wprowadź swój klucz API Gemini", "openaiCompatibleProvider": "Kompatybilny z OpenAI", "openaiCompatibleBaseUrlLabel": "Bazowy URL:", "openaiCompatibleApiKeyLabel": "Klucz API:", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index be7bfe2d0d..accb3a0c95 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -44,6 +44,9 @@ "selectProviderPlaceholder": "Selecionar provedor", "openaiProvider": "OpenAI", "ollamaProvider": "Ollama", + "geminiProvider": "Gemini", + "geminiApiKeyLabel": "Chave de API:", + "geminiApiKeyPlaceholder": "Digite sua chave de API do Gemini", "openaiCompatibleProvider": "Compatível com OpenAI", "openaiCompatibleBaseUrlLabel": "URL Base:", "openaiCompatibleApiKeyLabel": "Chave de API:", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index b0693f4532..65f308b5d6 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -44,6 +44,9 @@ "selectProviderPlaceholder": "Выберите провайдера", "openaiProvider": "OpenAI", "ollamaProvider": "Ollama", + "geminiProvider": "Gemini", + "geminiApiKeyLabel": "Ключ API:", + "geminiApiKeyPlaceholder": "Введите свой API-ключ Gemini", "openaiCompatibleProvider": "OpenAI-совместимый", "openaiCompatibleBaseUrlLabel": "Базовый URL:", "openaiCompatibleApiKeyLabel": "Ключ API:", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 06bd626406..6adbbd0369 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -44,6 +44,9 @@ "selectProviderPlaceholder": "Sağlayıcı seç", "openaiProvider": "OpenAI", "ollamaProvider": "Ollama", + "geminiProvider": "Gemini", + "geminiApiKeyLabel": "API Anahtarı:", + "geminiApiKeyPlaceholder": "Gemini API anahtarınızı girin", "openaiCompatibleProvider": "OpenAI Uyumlu", "openaiCompatibleBaseUrlLabel": "Temel URL:", "openaiCompatibleApiKeyLabel": "API Anahtarı:", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index c434276b3b..ab0b0f8215 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -44,6 +44,9 @@ "selectProviderPlaceholder": "Chọn nhà cung cấp", "openaiProvider": "OpenAI", "ollamaProvider": "Ollama", + "geminiProvider": "Gemini", + "geminiApiKeyLabel": "Khóa API:", + "geminiApiKeyPlaceholder": "Nhập khóa API Gemini của bạn", "openaiCompatibleProvider": "Tương thích OpenAI", "openaiCompatibleBaseUrlLabel": "URL cơ sở:", "openaiCompatibleApiKeyLabel": "Khóa API:", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 80bcab26eb..39e19b2def 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -44,6 +44,9 @@ "selectProviderPlaceholder": "选择提供商", "openaiProvider": "OpenAI", "ollamaProvider": "Ollama", + "geminiProvider": "Gemini", + "geminiApiKeyLabel": "API 密钥:", + "geminiApiKeyPlaceholder": "输入您的Gemini API密钥", "openaiCompatibleProvider": "OpenAI 兼容", "openaiCompatibleBaseUrlLabel": "基础 URL:", "openaiCompatibleApiKeyLabel": "API 密钥:", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 033230ebb4..441087e78a 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -44,6 +44,9 @@ "selectProviderPlaceholder": "選擇提供者", "openaiProvider": "OpenAI", "ollamaProvider": "Ollama", + "geminiProvider": "Gemini", + "geminiApiKeyLabel": "API 金鑰:", + "geminiApiKeyPlaceholder": "輸入您的Gemini API金鑰", "openaiCompatibleProvider": "OpenAI 相容", "openaiCompatibleBaseUrlLabel": "基礎 URL:", "openaiCompatibleApiKeyLabel": "API 金鑰:",