diff --git a/packages/types/src/codebase-index.ts b/packages/types/src/codebase-index.ts index e86c17627f..4b02cf2847 100644 --- a/packages/types/src/codebase-index.ts +++ b/packages/types/src/codebase-index.ts @@ -7,9 +7,11 @@ 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(), + geminiEmbeddingTaskType: z.string().optional(), + geminiEmbeddingDimension: z.number().optional(), }) export type CodebaseIndexConfig = z.infer @@ -22,6 +24,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 diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 65e3f9b5b6..36b9178514 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -151,6 +151,8 @@ const lmStudioSchema = baseProviderSettingsSchema.extend({ const geminiSchema = apiModelIdProviderModelSchema.extend({ geminiApiKey: z.string().optional(), googleGeminiBaseUrl: z.string().optional(), + geminiEmbeddingTaskType: z.string().optional(), + geminiEmbeddingDimension: z.number().optional(), }) const openAiNativeSchema = apiModelIdProviderModelSchema.extend({ @@ -258,6 +260,7 @@ export const providerSettingsSchema = z.object({ }) export type ProviderSettings = z.infer + export const PROVIDER_SETTINGS_KEYS = providerSettingsSchema.keyof().options export const MODEL_ID_KEYS: Partial[] = [ diff --git a/scripts/update-contributors.js b/scripts/update-contributors.js index 6bd4c35f0c..64ade5bea2 100644 --- a/scripts/update-contributors.js +++ b/scripts/update-contributors.js @@ -183,14 +183,14 @@ async function readReadme() { * @param {Array} contributors Array of contributor objects from GitHub API * @returns {string} HTML for contributors section */ -const EXCLUDED_LOGIN_SUBSTRINGS = ['[bot]', 'R00-B0T']; -const EXCLUDED_LOGIN_EXACTS = ['cursor', 'roomote']; +const EXCLUDED_LOGIN_SUBSTRINGS = ["[bot]", "R00-B0T"] +const EXCLUDED_LOGIN_EXACTS = ["cursor", "roomote"] function formatContributorsSection(contributors) { // Filter out GitHub Actions bot, cursor, and roomote - const filteredContributors = contributors.filter((c) => - !EXCLUDED_LOGIN_SUBSTRINGS.some(sub => c.login.includes(sub)) && - !EXCLUDED_LOGIN_EXACTS.includes(c.login) + const filteredContributors = contributors.filter( + (c) => + !EXCLUDED_LOGIN_SUBSTRINGS.some((sub) => c.login.includes(sub)) && !EXCLUDED_LOGIN_EXACTS.includes(c.login), ) // Start building with Markdown table format diff --git a/src/api/providers/gemini.ts b/src/api/providers/gemini.ts index 6765c8676d..c5b3264ee2 100644 --- a/src/api/providers/gemini.ts +++ b/src/api/providers/gemini.ts @@ -26,7 +26,7 @@ type GeminiHandlerOptions = ApiHandlerOptions & { export class GeminiHandler extends BaseProvider implements SingleCompletionHandler { protected options: ApiHandlerOptions - private client: GoogleGenAI + protected client: GoogleGenAI constructor({ isVertex, ...options }: GeminiHandlerOptions) { super() diff --git a/src/api/providers/xai.ts b/src/api/providers/xai.ts index 596c9e89b8..8f88351584 100644 --- a/src/api/providers/xai.ts +++ b/src/api/providers/xai.ts @@ -78,12 +78,15 @@ export class XAIHandler extends BaseProvider implements SingleCompletionHandler if (chunk.usage) { // Extract detailed token information if available // First check for prompt_tokens_details structure (real API response) - const promptDetails = "prompt_tokens_details" in chunk.usage ? chunk.usage.prompt_tokens_details : null; - const cachedTokens = promptDetails && "cached_tokens" in promptDetails ? promptDetails.cached_tokens : 0; + const promptDetails = "prompt_tokens_details" in chunk.usage ? chunk.usage.prompt_tokens_details : null + const cachedTokens = promptDetails && "cached_tokens" in promptDetails ? promptDetails.cached_tokens : 0 // Fall back to direct fields in usage (used in test mocks) - const readTokens = cachedTokens || ("cache_read_input_tokens" in chunk.usage ? (chunk.usage as any).cache_read_input_tokens : 0); - const writeTokens = "cache_creation_input_tokens" in chunk.usage ? (chunk.usage as any).cache_creation_input_tokens : 0; + const readTokens = + cachedTokens || + ("cache_read_input_tokens" in chunk.usage ? (chunk.usage as any).cache_read_input_tokens : 0) + const writeTokens = + "cache_creation_input_tokens" in chunk.usage ? (chunk.usage as any).cache_creation_input_tokens : 0 yield { type: "usage", diff --git a/src/services/code-index/__tests__/config-manager.spec.ts b/src/services/code-index/__tests__/config-manager.spec.ts index f5a759c158..dacde156d7 100644 --- a/src/services/code-index/__tests__/config-manager.spec.ts +++ b/src/services/code-index/__tests__/config-manager.spec.ts @@ -36,6 +36,14 @@ describe("CodeIndexConfigManager", () => { modelId: undefined, openAiOptions: { openAiNativeApiKey: "" }, ollamaOptions: { ollamaBaseUrl: "" }, + geminiOptions: { + apiModelId: undefined, + geminiApiKey: "", + geminiEmbeddingDimension: undefined, + geminiEmbeddingTaskType: undefined, + rateLimitSeconds: undefined, + }, + openAiCompatibleOptions: undefined, qdrantUrl: "http://localhost:6333", qdrantApiKey: "", searchMinScore: 0.4, @@ -67,6 +75,20 @@ describe("CodeIndexConfigManager", () => { modelId: "text-embedding-3-large", openAiOptions: { openAiNativeApiKey: "test-openai-key" }, ollamaOptions: { ollamaBaseUrl: "" }, + geminiOptions: { + apiModelId: "text-embedding-3-large", + geminiApiKey: "", + geminiEmbeddingDimension: undefined, + geminiEmbeddingTaskType: undefined, + rateLimitSeconds: { + codebaseIndexEnabled: true, + codebaseIndexQdrantUrl: "http://qdrant.local", + codebaseIndexEmbedderProvider: "openai", + codebaseIndexEmbedderBaseUrl: "", + codebaseIndexEmbedderModelId: "text-embedding-3-large", + }, + }, + openAiCompatibleOptions: undefined, qdrantUrl: "http://qdrant.local", qdrantApiKey: "test-qdrant-key", searchMinScore: 0.4, @@ -101,9 +123,17 @@ describe("CodeIndexConfigManager", () => { modelId: "text-embedding-3-large", openAiOptions: { openAiNativeApiKey: "" }, ollamaOptions: { ollamaBaseUrl: "" }, + geminiOptions: { + apiModelId: "text-embedding-3-large", + geminiApiKey: "", + geminiEmbeddingDimension: undefined, + geminiEmbeddingTaskType: undefined, + rateLimitSeconds: undefined, + }, openAiCompatibleOptions: { baseUrl: "https://api.example.com/v1", apiKey: "test-openai-compatible-key", + modelDimension: undefined, }, qdrantUrl: "http://qdrant.local", qdrantApiKey: "test-qdrant-key", @@ -140,6 +170,13 @@ describe("CodeIndexConfigManager", () => { modelId: "custom-model", openAiOptions: { openAiNativeApiKey: "" }, ollamaOptions: { ollamaBaseUrl: "" }, + geminiOptions: { + apiModelId: "custom-model", + geminiApiKey: "", + geminiEmbeddingDimension: undefined, + geminiEmbeddingTaskType: undefined, + rateLimitSeconds: undefined, + }, openAiCompatibleOptions: { baseUrl: "https://api.example.com/v1", apiKey: "test-openai-compatible-key", @@ -180,9 +217,17 @@ describe("CodeIndexConfigManager", () => { modelId: "custom-model", openAiOptions: { openAiNativeApiKey: "" }, ollamaOptions: { ollamaBaseUrl: "" }, + geminiOptions: { + apiModelId: "custom-model", + geminiApiKey: "", + geminiEmbeddingDimension: undefined, + geminiEmbeddingTaskType: undefined, + rateLimitSeconds: undefined, + }, openAiCompatibleOptions: { baseUrl: "https://api.example.com/v1", apiKey: "test-openai-compatible-key", + modelDimension: undefined, }, qdrantUrl: "http://qdrant.local", qdrantApiKey: "test-qdrant-key", @@ -219,6 +264,13 @@ describe("CodeIndexConfigManager", () => { modelId: "custom-model", openAiOptions: { openAiNativeApiKey: "" }, ollamaOptions: { ollamaBaseUrl: "" }, + geminiOptions: { + apiModelId: "custom-model", + geminiApiKey: "", + geminiEmbeddingDimension: undefined, + geminiEmbeddingTaskType: undefined, + rateLimitSeconds: undefined, + }, openAiCompatibleOptions: { baseUrl: "https://api.example.com/v1", apiKey: "test-openai-compatible-key", @@ -939,6 +991,19 @@ describe("CodeIndexConfigManager", () => { modelId: "text-embedding-3-large", openAiOptions: { openAiNativeApiKey: "test-openai-key" }, ollamaOptions: { ollamaBaseUrl: undefined }, + geminiEmbeddingDimension: undefined, + geminiOptions: { + apiModelId: "text-embedding-3-large", + geminiApiKey: "", + geminiEmbeddingDimension: undefined, + geminiEmbeddingTaskType: undefined, + rateLimitSeconds: { + codebaseIndexEnabled: true, + codebaseIndexQdrantUrl: "http://qdrant.local", + codebaseIndexEmbedderProvider: "openai", + codebaseIndexEmbedderModelId: "text-embedding-3-large", + }, + }, qdrantUrl: "http://qdrant.local", qdrantApiKey: "test-qdrant-key", searchMinScore: 0.4, @@ -967,69 +1032,70 @@ describe("CodeIndexConfigManager", () => { describe("initialization and restart prevention", () => { it("should not require restart when configuration hasn't changed between calls", async () => { - // Setup initial configuration - start with enabled and configured to avoid initial transition restart - mockContextProxy.getGlobalState.mockReturnValue({ + const config = { codebaseIndexEnabled: true, codebaseIndexQdrantUrl: "http://qdrant.local", codebaseIndexEmbedderProvider: "openai", codebaseIndexEmbedderModelId: "text-embedding-3-small", - }) + } + mockContextProxy.getGlobalState.mockReturnValue(config) mockContextProxy.getSecret.mockImplementation((key: string) => { if (key === "codeIndexOpenAiKey") return "test-key" return undefined }) - // First load - this will initialize the config manager with current state - await configManager.loadConfiguration() + // First load + const result1 = await configManager.loadConfiguration() + expect(result1.requiresRestart).toBe(true) // Restarts on first valid config - // Second load with same configuration - should not require restart - const secondResult = await configManager.loadConfiguration() - expect(secondResult.requiresRestart).toBe(false) + // Second load with same config + const result2 = await configManager.loadConfiguration() + expect(result2.requiresRestart).toBe(false) }) it("should properly initialize with current config to prevent false restarts", async () => { - // Setup configuration - mockContextProxy.getGlobalState.mockReturnValue({ - codebaseIndexEnabled: false, // Start disabled to avoid transition restart + const config = { + codebaseIndexEnabled: true, codebaseIndexQdrantUrl: "http://qdrant.local", codebaseIndexEmbedderProvider: "openai", codebaseIndexEmbedderModelId: "text-embedding-3-small", - }) + } + mockContextProxy.getGlobalState.mockReturnValue(config) mockContextProxy.getSecret.mockImplementation((key: string) => { if (key === "codeIndexOpenAiKey") return "test-key" return undefined }) - // Create a new config manager (simulating what happens in CodeIndexManager.initialize) - const newConfigManager = new CodeIndexConfigManager(mockContextProxy) + // Initialize with the current config + await configManager.loadConfiguration() - // Load configuration - should not require restart since the manager should be initialized with current config - const result = await newConfigManager.loadConfiguration() + // First load should not require restart + const result = await configManager.loadConfiguration() expect(result.requiresRestart).toBe(false) }) it("should not require restart when settings are saved but code indexing config unchanged", async () => { - // This test simulates the original issue: handleExternalSettingsChange() being called - // when other settings are saved, but code indexing settings haven't changed - - // Setup initial state - enabled and configured - mockContextProxy.getGlobalState.mockReturnValue({ + // Initial config + const initialConfig = { codebaseIndexEnabled: true, codebaseIndexQdrantUrl: "http://qdrant.local", codebaseIndexEmbedderProvider: "openai", codebaseIndexEmbedderModelId: "text-embedding-3-small", - }) + } + mockContextProxy.getGlobalState.mockReturnValue(initialConfig) mockContextProxy.getSecret.mockImplementation((key: string) => { if (key === "codeIndexOpenAiKey") return "test-key" return undefined }) - // First load to establish baseline await configManager.loadConfiguration() - // Simulate external settings change where code indexing config hasn't changed - // (this is what happens when other settings are saved) - const result = await configManager.loadConfiguration() + // Simulate saving settings by creating a new manager and initializing + const newConfigManager = new CodeIndexConfigManager(mockContextProxy) + await newConfigManager.loadConfiguration() + + // Load config again with the same settings + const result = await newConfigManager.loadConfiguration() expect(result.requiresRestart).toBe(false) }) }) diff --git a/src/services/code-index/__tests__/service-factory.spec.ts b/src/services/code-index/__tests__/service-factory.spec.ts index a539549bad..f251733ec9 100644 --- a/src/services/code-index/__tests__/service-factory.spec.ts +++ b/src/services/code-index/__tests__/service-factory.spec.ts @@ -294,7 +294,7 @@ describe("CodeIndexServiceFactory", () => { factory.createVectorStore() // Assert - expect(mockGetModelDimension).toHaveBeenCalledWith("openai", testModelId) + expect(mockGetModelDimension).toHaveBeenCalledWith("openai", testModelId, undefined) expect(MockedQdrantVectorStore).toHaveBeenCalledWith( "/test/workspace", "http://localhost:6333", @@ -319,7 +319,7 @@ describe("CodeIndexServiceFactory", () => { factory.createVectorStore() // Assert - expect(mockGetModelDimension).toHaveBeenCalledWith("ollama", testModelId) + expect(mockGetModelDimension).toHaveBeenCalledWith("ollama", testModelId, undefined) expect(MockedQdrantVectorStore).toHaveBeenCalledWith( "/test/workspace", "http://localhost:6333", @@ -469,7 +469,7 @@ describe("CodeIndexServiceFactory", () => { factory.createVectorStore() // Assert - expect(mockGetModelDimension).toHaveBeenCalledWith("openai", "default-model") + expect(mockGetModelDimension).toHaveBeenCalledWith("openai", "default-model", undefined) expect(MockedQdrantVectorStore).toHaveBeenCalledWith( "/test/workspace", "http://localhost:6333", diff --git a/src/services/code-index/config-manager.ts b/src/services/code-index/config-manager.ts index 678cec36a1..0817c57fbf 100644 --- a/src/services/code-index/config-manager.ts +++ b/src/services/code-index/config-manager.ts @@ -3,7 +3,19 @@ import { ContextProxy } from "../../core/config/ContextProxy" import { EmbedderProvider } from "./interfaces/manager" import { CodeIndexConfig, PreviousConfigSnapshot } from "./interfaces/config" import { SEARCH_MIN_SCORE } from "./constants" -import { getDefaultModelId, getModelDimension } from "../../shared/embeddingModels" +import { getDefaultModelId, getModelDimension, EMBEDDING_MODEL_PROFILES } from "../../shared/embeddingModels" + +// Define a type for the raw config state from globalState +interface RawCodebaseIndexConfigState { + codebaseIndexEnabled?: boolean + codebaseIndexQdrantUrl?: string + codebaseIndexSearchMinScore?: number // Assuming this is also from globalState based on default + codebaseIndexEmbedderProvider?: "openai" | "ollama" | "openai-compatible" | "gemini" + codebaseIndexEmbedderBaseUrl?: string + codebaseIndexEmbedderModelId?: string + geminiEmbeddingTaskType?: string + geminiEmbeddingDimension?: number // Ensure this is part of the raw state type +} /** * Manages configuration state and validation for the code indexing feature. @@ -16,6 +28,8 @@ export class CodeIndexConfigManager { private openAiOptions?: ApiHandlerOptions private ollamaOptions?: ApiHandlerOptions private openAiCompatibleOptions?: { baseUrl: string; apiKey: string; modelDimension?: number } + private geminiOptions?: ApiHandlerOptions + private qdrantUrl?: string = "http://localhost:6333" private qdrantApiKey?: string private searchMinScore?: number @@ -31,14 +45,16 @@ export class CodeIndexConfigManager { */ private _loadAndSetConfiguration(): void { // Load configuration from storage - const codebaseIndexConfig = this.contextProxy?.getGlobalState("codebaseIndexConfig") ?? { + const rawConfig = (this.contextProxy?.getGlobalState("codebaseIndexConfig") ?? { codebaseIndexEnabled: false, codebaseIndexQdrantUrl: "http://localhost:6333", codebaseIndexSearchMinScore: 0.4, codebaseIndexEmbedderProvider: "openai", codebaseIndexEmbedderBaseUrl: "", codebaseIndexEmbedderModelId: "", - } + geminiEmbeddingTaskType: undefined, + geminiEmbeddingDimension: undefined, + }) as RawCodebaseIndexConfigState // Cast to our defined raw state type const { codebaseIndexEnabled, @@ -46,7 +62,9 @@ export class CodeIndexConfigManager { codebaseIndexEmbedderProvider, codebaseIndexEmbedderBaseUrl, codebaseIndexEmbedderModelId, - } = codebaseIndexConfig + geminiEmbeddingTaskType, + geminiEmbeddingDimension, + } = rawConfig // Destructure from the typed rawConfig const openAiKey = this.contextProxy?.getSecret("codeIndexOpenAiKey") ?? "" const qdrantApiKey = this.contextProxy?.getSecret("codeIndexQdrantApiKey") ?? "" @@ -56,6 +74,9 @@ export class CodeIndexConfigManager { "codebaseIndexOpenAiCompatibleModelDimension", ) as number | undefined + const geminiApiKey = this.contextProxy?.getSecret("geminiApiKey") ?? "" + const rateLimitSeconds = this.contextProxy?.getGlobalState("rateLimitSeconds") ?? undefined + // Update instance variables with configuration this.isEnabled = codebaseIndexEnabled || false this.qdrantUrl = codebaseIndexQdrantUrl @@ -68,6 +89,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 +109,14 @@ export class CodeIndexConfigManager { modelDimension: openAiCompatibleModelDimension, } : undefined + + this.geminiOptions = { + geminiApiKey, + geminiEmbeddingTaskType: geminiEmbeddingTaskType, + apiModelId: this.modelId, + geminiEmbeddingDimension, + rateLimitSeconds, + } } /** @@ -101,6 +132,7 @@ export class CodeIndexConfigManager { openAiOptions?: ApiHandlerOptions ollamaOptions?: ApiHandlerOptions openAiCompatibleOptions?: { baseUrl: string; apiKey: string } + geminiOptions?: ApiHandlerOptions qdrantUrl?: string qdrantApiKey?: string searchMinScore?: number @@ -118,6 +150,8 @@ export class CodeIndexConfigManager { openAiCompatibleBaseUrl: this.openAiCompatibleOptions?.baseUrl ?? "", openAiCompatibleApiKey: this.openAiCompatibleOptions?.apiKey ?? "", openAiCompatibleModelDimension: this.openAiCompatibleOptions?.modelDimension, + geminiApiKey: this.geminiOptions?.geminiApiKey, + geminiEmbeddingTaskType: this.geminiOptions?.geminiEmbeddingTaskType, qdrantUrl: this.qdrantUrl ?? "", qdrantApiKey: this.qdrantApiKey ?? "", } @@ -137,6 +171,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, @@ -166,6 +201,25 @@ export class CodeIndexConfigManager { const qdrantUrl = this.qdrantUrl return !!(baseUrl && apiKey && qdrantUrl) } + + if (this.embedderProvider === "gemini") { + // Gemini requires an API key and Qdrant URL + const geminiApiKey = this.geminiOptions?.geminiApiKey + const modelId = this.modelId || getDefaultModelId("gemini") + const qdrantUrl = this.qdrantUrl + + // Check if the model supports taskType + const geminiProfiles = EMBEDDING_MODEL_PROFILES.gemini || {} + const modelProfile = geminiProfiles[modelId] + const supportsTaskType = modelProfile?.supportsTaskType || false + + // Only require taskType if the model supports it + const geminiEmbeddingTaskType = this.geminiOptions?.geminiEmbeddingTaskType + const taskTypeValid = !supportsTaskType || (supportsTaskType && !!geminiEmbeddingTaskType) + + const isConfigured = !!(geminiApiKey && taskTypeValid && qdrantUrl) + return isConfigured + } return false // Should not happen if embedderProvider is always set correctly } @@ -185,6 +239,8 @@ export class CodeIndexConfigManager { const prevOpenAiCompatibleBaseUrl = prev?.openAiCompatibleBaseUrl ?? "" const prevOpenAiCompatibleApiKey = prev?.openAiCompatibleApiKey ?? "" const prevOpenAiCompatibleModelDimension = prev?.openAiCompatibleModelDimension + const prevGeminiApiKey = prev?.geminiApiKey ?? "" + const prevGeminiEmbeddingDimension = prev?.geminiEmbeddingDimension // Access from prev const prevQdrantUrl = prev?.qdrantUrl ?? "" const prevQdrantApiKey = prev?.qdrantApiKey ?? "" @@ -210,7 +266,9 @@ export class CodeIndexConfigManager { return true } - if (this._hasVectorDimensionChanged(prevProvider, prevModelId)) { + // Check for dimension change, including the new geminiEmbeddingDimension + if (this._hasVectorDimensionChanged(prevProvider, prevModelId, prev.geminiEmbeddingDimension)) { + // Use prev.geminiEmbeddingDimension return true } @@ -242,6 +300,18 @@ export class CodeIndexConfigManager { } } + if (this.embedderProvider === "gemini") { + const currentGeminiApiKey = this.geminiOptions?.geminiApiKey ?? "" + if (prevGeminiApiKey !== currentGeminiApiKey) { + return true + } + + const currentGeminiEmbeddingDimension = this.geminiOptions?.geminiEmbeddingDimension + if (currentGeminiEmbeddingDimension !== prevGeminiEmbeddingDimension) { + return true + } + } + // Qdrant configuration changes const currentQdrantUrl = this.qdrantUrl ?? "" const currentQdrantApiKey = this.qdrantApiKey ?? "" @@ -257,19 +327,35 @@ export class CodeIndexConfigManager { /** * Checks if model changes result in vector dimension changes that require restart. */ - private _hasVectorDimensionChanged(prevProvider: EmbedderProvider, prevModelId?: string): boolean { + private _hasVectorDimensionChanged( + prevProvider: EmbedderProvider, + prevModelId?: string, + prevGeminiDimension?: number, + ): boolean { const currentProvider = this.embedderProvider const currentModelId = this.modelId ?? getDefaultModelId(currentProvider) const resolvedPrevModelId = prevModelId ?? getDefaultModelId(prevProvider) // If model IDs are the same and provider is the same, no dimension change if (prevProvider === currentProvider && resolvedPrevModelId === currentModelId) { + // If provider and model are same, check if gemini dimension changed + if (currentProvider === "gemini" && this.geminiOptions?.geminiEmbeddingDimension !== prevGeminiDimension) { + return true + } return false } // Get vector dimensions for both models - const prevDimension = getModelDimension(prevProvider, resolvedPrevModelId) - const currentDimension = getModelDimension(currentProvider, currentModelId) + const prevDimension = getModelDimension( + prevProvider, + resolvedPrevModelId, + prevProvider === "gemini" ? prevGeminiDimension : undefined, + ) + const currentDimension = getModelDimension( + currentProvider, + currentModelId, + currentProvider === "gemini" ? this.geminiOptions?.geminiEmbeddingDimension : undefined, + ) // If we can't determine dimensions, be safe and restart if (prevDimension === undefined || currentDimension === undefined) { @@ -292,9 +378,11 @@ export class CodeIndexConfigManager { openAiOptions: this.openAiOptions, ollamaOptions: this.ollamaOptions, openAiCompatibleOptions: this.openAiCompatibleOptions, + geminiOptions: this.geminiOptions, qdrantUrl: this.qdrantUrl, qdrantApiKey: this.qdrantApiKey, searchMinScore: this.searchMinScore, + geminiEmbeddingDimension: this.geminiOptions?.geminiEmbeddingDimension, } } diff --git a/src/services/code-index/constants/index.ts b/src/services/code-index/constants/index.ts index cbf6941817..84fa5dd317 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_RATE_LIMIT_DELAY_MS = 6000 diff --git a/src/services/code-index/embedders/gemini.ts b/src/services/code-index/embedders/gemini.ts new file mode 100644 index 0000000000..18be2bdc02 --- /dev/null +++ b/src/services/code-index/embedders/gemini.ts @@ -0,0 +1,262 @@ +import { ApiHandlerOptions } from "../../../shared/api" +import { EmbedderInfo, EmbeddingResponse, IEmbedder } from "../interfaces" +import { GeminiHandler } from "../../../api/providers/gemini" +import { EMBEDDING_MODEL_PROFILES } from "../../../shared/embeddingModels" +import { GEMINI_RATE_LIMIT_DELAY_MS } from "../constants" +import { SlidingWindowRateLimiter, SlidingWindowRateLimiterOptions } from "../../../utils/rate-limiter" +import { RetryHandler } from "../../../utils/retry-handler" + +/** + * Implements the IEmbedder interface using Google Gemini's embedding API. + */ +export class CodeIndexGeminiEmbedder extends GeminiHandler implements IEmbedder { + private readonly defaultModelId: string + private readonly defaultTaskType?: string + private readonly rateLimiter: SlidingWindowRateLimiter + private readonly retryHandler: RetryHandler + + /** + * Creates a new Gemini embedder instance. + * @param options API handler options containing Gemini configurations + */ + constructor(options: ApiHandlerOptions) { + super(options) + this.defaultModelId = options.apiModelId || "gemini-embedding-exp-03-07" + this.defaultTaskType = options.geminiEmbeddingTaskType + + // Calculate rate limit parameters based on rateLimitSeconds or default + const rateLimitSeconds = options.rateLimitSeconds || GEMINI_RATE_LIMIT_DELAY_MS / 1000 + + // Configure the rate limiter to use rateLimitSeconds for rate calculations + const limiterOptions: SlidingWindowRateLimiterOptions = { + rateLimitSeconds: rateLimitSeconds, + } + + // Get the singleton rate limiter instance + this.rateLimiter = new SlidingWindowRateLimiter(limiterOptions) + // Initialize retry handler with default options + this.retryHandler = new RetryHandler({ + initialDelay: rateLimitSeconds, + }) + } + + /** + * Creates embeddings for the given texts using the Gemini API, ensuring sequential processing. + * @param texts - An array of strings to embed. + * @param model - Optional model ID to override the default. + * @returns A promise that resolves to an EmbeddingResponse containing the embeddings. + */ + async createEmbeddings(texts: string[], model?: string): Promise { + try { + const modelId = model || this.defaultModelId + const result = await this.embedWithTokenLimit(texts, modelId, this.defaultTaskType || "") + return { + embeddings: result.embeddings, + } + } catch (error: any) { + console.error("Error during Gemini embedding task execution in queue:", error.message) + throw error + } + } + + /** + * Processes a batch of texts and aggregates the embeddings and usage statistics. + * + * @param batch Array of texts to process + * @param model Model identifier to use + * @param taskType The task type for the embedding + * @param allEmbeddings Array to store all embeddings + * @param aggregatedUsage Object to track token usage + * @param isFinalBatch Whether this is the final batch (affects error messages) + */ + private async _processAndAggregateBatch( + batch: string[], + model: string, + taskType: string = "", + allEmbeddings: number[][], + aggregatedUsage: { promptTokens: number; totalTokens: number }, + isFinalBatch: boolean = false, + ): Promise { + if (batch.length === 0) return + + try { + const batchResult = await this._embedBatch(batch, model, taskType) + allEmbeddings.push(...batchResult.embeddings) + aggregatedUsage.promptTokens += batchResult.usage.promptTokens + aggregatedUsage.totalTokens += batchResult.usage.totalTokens + } catch (error) { + const batchType = isFinalBatch ? "final batch" : "batch" + console.error(`Failed to process ${batchType} with retries:`, error) + throw new Error(`Failed to create embeddings for ${batchType}: ${(error as Error).message}`) + } + } + + /** + * Embeds texts while respecting the token limit of the model. + * Splits the input texts into batches that don't exceed the model's token limit. + * Also adds a delay between requests to respect Gemini's rate limits. + * + * @param texts - Array of text strings to create embeddings for + * @param model - Model ID to use for embeddings + * @param taskType - The task type to optimize embeddings for + * @returns Promise resolving to an object with embeddings and usage data + */ + private async embedWithTokenLimit( + texts: string[], + model: string, + taskType: string = "", + ): Promise<{ + embeddings: number[][] + usage: { promptTokens: number; totalTokens: number } + }> { + // Get the model profile + const geminiProfiles = EMBEDDING_MODEL_PROFILES.gemini || {} + const modelProfile = geminiProfiles[model] + + // Default max tokens if not specified in the profile + const maxInputTokens = modelProfile?.maxInputTokens || 8192 + + // Initialize result arrays + const allEmbeddings: number[][] = [] + const aggregatedUsage = { promptTokens: 0, totalTokens: 0 } + + // Initialize the current batch + let currentBatch: string[] = [] + let currentBatchTokens = 0 + + // Process each text sequentially with for...of loop + for (const text of texts) { + // Estimate tokens (similar to OpenAI's implementation) + const estimatedTokens = Math.ceil(text.length / 4) + + // Skip texts that exceed the max token limit for a single item + if (estimatedTokens > maxInputTokens) { + console.warn(`Text exceeds maximum token limit (${estimatedTokens} > ${maxInputTokens}). Skipping.`) + continue + } + + // If adding this text would exceed the token limit, process the current batch first + if (currentBatchTokens + estimatedTokens > maxInputTokens) { + // Process the current batch + await this._processAndAggregateBatch(currentBatch, model, taskType, allEmbeddings, aggregatedUsage) + + // Reset the batch + currentBatch = [] + currentBatchTokens = 0 + } + + // Add the current text to the batch + currentBatch.push(text) + currentBatchTokens += estimatedTokens + } + + // Process any remaining texts in the final batch + await this._processAndAggregateBatch(currentBatch, model, taskType, allEmbeddings, aggregatedUsage, true) + + return { embeddings: allEmbeddings, usage: aggregatedUsage } + } + + /** + * Makes the actual API call to Gemini's embedding service and processes the response. + * + * @param batchTexts Array of texts to embed + * @param modelId Model identifier to use for the API call + * @param taskType The task type for the embedding (only used if the model supports it) + * @returns Promise resolving to embeddings and usage statistics + */ + private async _callGeminiEmbeddingApi( + batchTexts: string[], + modelId: string, + taskType: string, + ): Promise<{ embeddings: number[][]; usage: { promptTokens: number; totalTokens: number } }> { + // Check if the model supports taskType + const geminiProfiles = EMBEDDING_MODEL_PROFILES.gemini || {} + const modelProfile = geminiProfiles[modelId] + const supportsTaskType = modelProfile?.supportsTaskType || false + + // Only include taskType in the config if the model supports it + const config: { taskType?: string } = {} + if (supportsTaskType) { + config.taskType = taskType + } + + const response = await this.client.models.embedContent({ + model: modelId, + contents: batchTexts, + config, + }) + + if (!response.embeddings) { + throw new Error("No embeddings returned from Gemini API") + } + + const embeddings = response.embeddings + .map((embedding) => embedding?.values) + .filter((values) => values !== undefined && values.length > 0) as number[][] + + // Gemini API for embeddings doesn't directly return token usage per call + return { + embeddings, + usage: { promptTokens: 0, totalTokens: 0 }, // Placeholder usage + } + } + + /** + * Creates embeddings for a batch of texts using the Gemini API. + * Rate limiting is handled by the SlidingWindowRateLimiter. + * + * @param batchTexts Array of texts to embed in this batch + * @param model Model identifier to use + * @param taskType The task type for the embedding + * @returns Promise resolving to embeddings and usage statistics + */ + private async _embedBatch( + batchTexts: string[], + model: string, + taskType: string = "", + ): Promise<{ embeddings: number[][]; usage: { promptTokens: number; totalTokens: number } }> { + const modelId = model || this.defaultModelId + + // Determine if an error is retryable (429 Too Many Requests or specific API errors) + const shouldRetry = (error: any): boolean => { + const retryable = + error.status === 429 || + error.message?.includes("RESOURCE_EXHAUSTED") || + error.message?.includes("rate limit") || + error.message?.includes("quota exceeded") + + if (retryable) { + console.log(`Retryable error detected: ${error.message}`) + } + + return retryable + } + + try { + // Execute the API call with retry logic + return await this.retryHandler.execute(async () => { + // Acquire a slot from the rate limiter before making the API call + // This ensures each retry attempt also respects rate limits + await this.rateLimiter.acquire() + return await this._callGeminiEmbeddingApi(batchTexts, modelId, taskType) + }, shouldRetry) + } catch (error: any) { + // Log the error with context + console.error(`Gemini embedding request failed after all retry attempts:`, { + error: error.message, + status: error.status, + modelId, + batchSize: batchTexts.length, + }) + + // Rethrow the error + throw error + } + } + + get embedderInfo(): EmbedderInfo { + return { + name: "gemini", + } + } +} diff --git a/src/services/code-index/interfaces/config.ts b/src/services/code-index/interfaces/config.ts index 0843120fd9..c149e389df 100644 --- a/src/services/code-index/interfaces/config.ts +++ b/src/services/code-index/interfaces/config.ts @@ -12,9 +12,11 @@ export interface CodeIndexConfig { openAiOptions?: ApiHandlerOptions ollamaOptions?: ApiHandlerOptions openAiCompatibleOptions?: { baseUrl: string; apiKey: string; modelDimension?: number } + geminiOptions?: ApiHandlerOptions qdrantUrl?: string qdrantApiKey?: string searchMinScore?: number + geminiEmbeddingDimension?: number } /** @@ -30,6 +32,9 @@ export type PreviousConfigSnapshot = { openAiCompatibleBaseUrl?: string openAiCompatibleApiKey?: string openAiCompatibleModelDimension?: number + geminiApiKey?: string + geminiEmbeddingTaskType?: string + geminiEmbeddingDimension?: number 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..149bfe2c1c 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 { CodeIndexGeminiEmbedder } 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?.geminiApiKey) { + throw new Error("Gemini configuration missing for embedder creation") + } + return new CodeIndexGeminiEmbedder(config.geminiOptions) } throw new Error(`Invalid embedder type configured: ${config.embedderProvider}`) @@ -68,6 +74,10 @@ export class CodeIndexServiceFactory { const defaultModel = getDefaultModelId(provider) // Use the embedding model ID from config, not the chat model IDs const modelId = config.modelId ?? defaultModel + let requestedDimension: number | undefined + if (provider === "gemini") { + requestedDimension = config.geminiEmbeddingDimension + } let vectorSize: number | undefined @@ -79,7 +89,7 @@ export class CodeIndexServiceFactory { vectorSize = getModelDimension(provider, modelId) } } else { - vectorSize = getModelDimension(provider, modelId) + vectorSize = getModelDimension(provider, modelId, requestedDimension) } if (vectorSize === undefined) { diff --git a/src/shared/embeddingModels.ts b/src/shared/embeddingModels.ts index cd7c1d4e6b..50663b9aef 100644 --- a/src/shared/embeddingModels.ts +++ b/src/shared/embeddingModels.ts @@ -2,10 +2,23 @@ * 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 + /** + * Specific dimensions supported by the model + */ + supportDimensions?: number[] + /** + * Optional maximum input tokens for the model. + */ + maxInputTokens?: number + /** + * Whether the model supports the taskType parameter. + * Only some Gemini models support this parameter. + */ + supportsTaskType?: boolean // Add other model-specific properties if needed, e.g., context window size } @@ -34,15 +47,30 @@ export const EMBEDDING_MODEL_PROFILES: EmbeddingModelProfiles = { "text-embedding-3-large": { dimension: 3072 }, "text-embedding-ada-002": { dimension: 1536 }, }, + gemini: { + "gemini-embedding-exp-03-07": { + dimension: 3072, + supportDimensions: [3072, 1536, 768], + maxInputTokens: 8192, + supportsTaskType: true, + }, + "models/text-embedding-004": { dimension: 768, maxInputTokens: 2048, supportsTaskType: false }, + "models/embedding-001": { dimension: 768, maxInputTokens: 2048, supportsTaskType: false }, + }, } /** * Retrieves the embedding dimension for a given provider and model ID. * @param provider The embedder provider (e.g., "openai"). * @param modelId The specific model ID (e.g., "text-embedding-3-small"). + * @param requestedDimension Optional dimension requested by the user. * @returns The dimension size or undefined if the model is not found. */ -export function getModelDimension(provider: EmbedderProvider, modelId: string): number | undefined { +export function getModelDimension( + provider: EmbedderProvider, + modelId: string, + requestedDimension?: number, +): number | undefined { const providerProfiles = EMBEDDING_MODEL_PROFILES[provider] if (!providerProfiles) { console.warn(`Provider not found in profiles: ${provider}`) @@ -56,6 +84,14 @@ export function getModelDimension(provider: EmbedderProvider, modelId: string): return undefined // Or potentially return a default/fallback dimension? } + if ( + requestedDimension && + modelProfile.supportDimensions && + modelProfile.supportDimensions.includes(requestedDimension) + ) { + return requestedDimension + } + return modelProfile.dimension } @@ -72,7 +108,8 @@ export function getDefaultModelId(provider: EmbedderProvider): string { case "openai": case "openai-compatible": return "text-embedding-3-small" - + case "gemini": + return "gemini-embedding-exp-03-07" case "ollama": { // Choose a sensible default for Ollama, e.g., the first one listed or a specific one const ollamaModels = EMBEDDING_MODEL_PROFILES.ollama diff --git a/src/utils/rate-limiter.ts b/src/utils/rate-limiter.ts new file mode 100644 index 0000000000..b52d2a7ff8 --- /dev/null +++ b/src/utils/rate-limiter.ts @@ -0,0 +1,203 @@ +/** + * Sliding Window Rate Limiter Implementation + * + * Justification for choosing the Sliding Window algorithm: + * + * 1. Accuracy: Unlike fixed window counters that can allow burst traffic at window + * boundaries, sliding window provides more consistent rate limiting by considering + * the actual time distribution of requests. + * + * 2. Memory Efficiency: Only stores timestamps within the current window period, + * automatically cleaning up old entries, making it memory-efficient for high-volume + * applications. + * + * 3. Fairness: Ensures a smooth rate of requests over time rather than allowing sudden + * spikes, which provides a more predictable system behavior and resource usage. + * + * 4. Adaptability: Can be easily adjusted to different time windows (minutes, seconds) + * while maintaining the same consistent behavior. + */ + +/** + * Configuration options for the SlidingWindowRateLimiter + */ +export interface SlidingWindowRateLimiterOptions { + /** + * The maximum number of requests allowed within the time window. + * If not provided, this will be calculated from rateLimitSeconds or default to 60. + */ + requestsPerWindow?: number + + /** + * The time window duration in milliseconds. Default is 60000 (1 minute). + * This is calculated from rateLimitSeconds if provided, otherwise uses this value directly. + */ + windowMs?: number + + /** + * The minimum time between requests in seconds. + * If provided, windowMs and requestsPerWindow will be calculated accordingly. + * For example, if rateLimitSeconds is 10, then it will allow 1 request per 10 seconds, + * or 6 requests per minute. + */ + rateLimitSeconds?: number +} + +export class SlidingWindowRateLimiter { + /** + * When `rateLimitSeconds` is supplied we want to guarantee a minimum spacing + * (a.k.a. cool-down) between consecutive requests. This value (in ms) + * represents that interval. If it is **undefined** we will fall back to the + * classic sliding-window algorithm that controls *average* throughput but can + * still allow bursts at the beginning of a window. + */ + private readonly minIntervalMs?: number + + private readonly requestsPerWindow: number + private readonly windowMs: number + private readonly requestTimestamps: number[] = [] + + /** + * The next timestamp (in ms) when a request is allowed if `minIntervalMs` is + * being enforced. + */ + private nextAvailableTime = 0 + + /** + * Internal chain used to serialize `acquire()` calls when a strict + * `minIntervalMs` is enforced. This guarantees that each caller observes the + * updated scheduling state (i.e. `nextAvailableTime`) and prevents the race + * condition where several concurrent calls all think a slot is immediately + * available. + */ + private serial: Promise = Promise.resolve() + + /** + * Creates a new instance of the SlidingWindowRateLimiter + * @param options Configuration options for the rate limiter + */ + constructor(options?: SlidingWindowRateLimiterOptions) { + if (options?.rateLimitSeconds) { + // When a strict minimum interval is required we will *also* keep the + // sliding-window counters so that extremely long-running burst scenarios + // are still prevented. + this.minIntervalMs = options.rateLimitSeconds * 1000 + + // Maintain a 2-minute window for additional protection against sustained + // high-traffic scenarios. This mirrors the previous behaviour while + // adding the per-request spacing guarantee. + const windowSizeInSeconds = 120 // 2 minutes + this.windowMs = windowSizeInSeconds * 1000 + this.requestsPerWindow = Math.floor(windowSizeInSeconds / options.rateLimitSeconds) + } else { + // Use provided values or defaults (pure sliding-window mode) + this.requestsPerWindow = options?.requestsPerWindow ?? 60 + this.windowMs = options?.windowMs ?? 60 * 1000 // Default: 1 minute in milliseconds + } + + // Validate final values + if (this.requestsPerWindow <= 0) { + throw new Error("Calculated requestsPerWindow must be a positive number") + } + } + + /** + * Acquires permission to proceed with a request + * + * @returns A promise that resolves when the request is permitted to proceed + */ + public async acquire(): Promise { + if (this.minIntervalMs !== undefined) { + // Capture to help TypeScript's control-flow analysis in the async body + const intervalMs = this.minIntervalMs! + + // Ensure **all** acquire() callers run through the following sequence one + // after another. + const nextInChain = this.serial.then(async () => { + const now = Date.now() + + // Calculate how long we need to wait so we keep at least minIntervalMs + // between *granted* requests. + const waitTime = Math.max(this.nextAvailableTime - now, 0) + if (waitTime > 0) { + await new Promise((r) => setTimeout(r, waitTime)) + } + + // Record when the next request is allowed *before* doing the heavy + // lifting so concurrent callers will see the updated schedule. + this.nextAvailableTime = Date.now() + intervalMs + + // Proceed with the regular sliding-window accounting. + await this.acquireWithoutMinInterval() + }) + + // Replace the promise chain but deliberately swallow errors in the chain + // so that a single failing acquire doesn't block others forever. The + // error is still propagated to the individual caller via `nextInChain`. + this.serial = nextInChain.catch(() => {}) + + return nextInChain + } + + // Fallback to pure sliding-window behaviour when no fixed interval is set. + return this.acquireWithoutMinInterval() + } + + /** + * The original acquire logic that only enforces the sliding-window quotas. It + * is extracted so that we can reuse it *after* satisfying the min-interval + * delay. + */ + private async acquireWithoutMinInterval(): Promise { + // Clean up expired timestamps that are outside the current window + this.cleanupExpiredTimestamps() + + if (this.requestTimestamps.length < this.requestsPerWindow) { + this.recordRequest() + + // Update the next allowed time if we have a min-interval configured. + if (this.minIntervalMs !== undefined) { + this.nextAvailableTime = Date.now() + this.minIntervalMs + } + + return + } + + // We are over the quota for the current window; wait until the oldest + // request timestamp exits the window. + return new Promise((resolve) => { + const oldestTimestamp = this.requestTimestamps[0] + const timeToWait = oldestTimestamp + this.windowMs - Date.now() + + setTimeout(() => { + this.cleanupExpiredTimestamps() + this.recordRequest() + + if (this.minIntervalMs !== undefined) { + this.nextAvailableTime = Date.now() + this.minIntervalMs + } + + resolve() + }, timeToWait + 1) + }) + } + + /** + * Records the current request timestamp and adds it to the sliding window + */ + private recordRequest(): void { + this.requestTimestamps.push(Date.now()) + } + + /** + * Removes timestamps that have fallen outside the current time window + */ + private cleanupExpiredTimestamps(): void { + const cutoffTime = Date.now() - this.windowMs + + // Remove all timestamps that are older than the cutoff time + while (this.requestTimestamps.length > 0 && this.requestTimestamps[0] <= cutoffTime) { + this.requestTimestamps.shift() + } + } +} diff --git a/src/utils/retry-handler.ts b/src/utils/retry-handler.ts new file mode 100644 index 0000000000..f025ea6092 --- /dev/null +++ b/src/utils/retry-handler.ts @@ -0,0 +1,159 @@ +/** + * Retry Handler with Exponential Backoff and Jitter + * + * This utility provides a configurable retry mechanism with exponential backoff strategy + * for handling transient failures in asynchronous operations. Key features include: + * + * 1. Exponential Backoff: Progressively increases the delay between retry attempts + * to reduce system load during recovery periods. + * + * 2. Maximum Delay Cap: Prevents retry delays from growing beyond a reasonable threshold. + * + * 3. Jitter: Optional randomization of delay times to prevent thundering herd problems + * when multiple clients retry simultaneously. + * + * 4. Selective Retry: Allows precise control over which errors should trigger a retry + * through a customizable predicate function. + */ + +/** + * Configuration options for the RetryHandler + */ +export interface RetryHandlerOptions { + /** + * The maximum number of retry attempts before giving up. + * Default: 5 + */ + maxRetries?: number + + /** + * The initial delay in milliseconds before the first retry. + * Default: 1000 (1 second) + */ + initialDelay?: number + + /** + * The maximum delay in milliseconds that the backoff is allowed to reach. + * Default: 30000 (30 seconds) + */ + maxDelay?: number + + /** + * The multiplier for the exponential backoff calculation. + * Each retry will wait approximately backoffFactor times longer than the previous attempt. + * Default: 2 + */ + backoffFactor?: number + + /** + * Whether to apply random jitter to the delay to prevent retry storms. + * When true, adds a random factor between 0.5 and 1.5 to the delay. + * Default: true + */ + jitter?: boolean +} + +export class RetryHandler { + private readonly maxRetries: number + private readonly initialDelay: number + private readonly maxDelay: number + private readonly backoffFactor: number + private readonly jitter: boolean + + /** + * Creates a new instance of the RetryHandler + * @param options Configuration options for the retry handler + */ + constructor(options?: RetryHandlerOptions) { + this.maxRetries = options?.maxRetries ?? 5 + this.initialDelay = options?.initialDelay ?? 1000 + this.maxDelay = options?.maxDelay ?? 30000 + this.backoffFactor = options?.backoffFactor ?? 2 + this.jitter = options?.jitter ?? true + + // Validate configuration values + if (this.maxRetries < 0) { + throw new Error("maxRetries must be a non-negative number") + } + if (this.initialDelay <= 0) { + throw new Error("initialDelay must be a positive number") + } + if (this.maxDelay <= 0) { + throw new Error("maxDelay must be a positive number") + } + if (this.maxDelay < this.initialDelay) { + throw new Error("maxDelay must be greater than or equal to initialDelay") + } + if (this.backoffFactor <= 0) { + throw new Error("backoffFactor must be a positive number") + } + } + + /** + * Executes the provided function with retry logic + * + * @param fn The asynchronous function to execute and retry on failure + * @param shouldRetry A predicate function that determines whether a given error should trigger a retry + * @returns A promise that resolves with the result of the function or rejects with the last error + */ + public async execute(fn: () => Promise, shouldRetry: (error: any) => boolean): Promise { + let lastError: any + + for (let attempt = 0; attempt <= this.maxRetries; attempt++) { + try { + // Attempt to execute the function + return await fn() + } catch (error) { + // Save the error for potential re-throw + lastError = error + + // If we've exhausted all retry attempts or shouldn't retry this error, give up + if (attempt >= this.maxRetries || !shouldRetry(error)) { + throw error + } + + // Calculate delay for the next retry + const delay = this.calculateDelay(attempt) + + // Wait before the next attempt + await this.wait(delay) + } + } + + // This should never be reached due to the throw in the loop, + // but TypeScript requires a return statement + throw lastError + } + + /** + * Calculates the delay for a specific retry attempt using exponential backoff + * and optional jitter + * + * @param attempt The current retry attempt (0-based) + * @returns The calculated delay in milliseconds + */ + private calculateDelay(attempt: number): number { + // Calculate base delay with exponential backoff: initialDelay * (backoffFactor ^ attempt) + let delay = this.initialDelay * Math.pow(this.backoffFactor, attempt) + + // Apply jitter if enabled + if (this.jitter) { + // Apply a random factor between 0.5 and 1.5 + const jitterFactor = 0.5 + Math.random() + delay *= jitterFactor + } + + // Ensure the delay doesn't exceed the maximum + return Math.min(delay, this.maxDelay) + } + + /** + * Returns a promise that resolves after the specified delay + * + * @param ms The delay in milliseconds + * @returns A promise that resolves after the delay + */ + private wait(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)) + } +} diff --git a/webview-ui/src/components/settings/CodeIndexSettings.tsx b/webview-ui/src/components/settings/CodeIndexSettings.tsx index 13c9524d9f..3b12d32e33 100644 --- a/webview-ui/src/components/settings/CodeIndexSettings.tsx +++ b/webview-ui/src/components/settings/CodeIndexSettings.tsx @@ -6,7 +6,7 @@ import { Trans } from "react-i18next" import { CodebaseIndexConfig, CodebaseIndexModels, ProviderSettings } from "@roo-code/types" -import { EmbedderProvider } from "@roo/embeddingModels" +import { EmbedderProvider, EMBEDDING_MODEL_PROFILES } from "@roo/embeddingModels" import { vscode } from "@src/utils/vscode" import { useAppTranslation } from "@src/i18n/TranslationContext" @@ -30,6 +30,7 @@ import { } from "@src/components/ui" import { SetCachedStateField } from "./types" +import { RateLimitSecondsControl } from "./RateLimitSecondsControl" interface CodeIndexSettingsProps { codebaseIndexModels: CodebaseIndexModels | undefined @@ -62,11 +63,22 @@ 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" + currentProvider === "openai" || + currentProvider === "ollama" || + currentProvider === "openai-compatible" || + currentProvider === "gemini" ? codebaseIndexModels?.[currentProvider] || codebaseIndexModels?.openai : codebaseIndexModels?.openai const availableModelIds = Object.keys(modelsForProvider || {}) + // Logic for Gemini dimension selector + const selectedModelIdForGeminiDim = codebaseIndexConfig?.codebaseIndexEmbedderModelId + const geminiModelProfileForDim = + currentProvider === "gemini" && selectedModelIdForGeminiDim + ? EMBEDDING_MODEL_PROFILES.gemini?.[selectedModelIdForGeminiDim] + : undefined + const geminiSupportedDims = geminiModelProfileForDim?.supportDimensions + useEffect(() => { // Request initial indexing status from extension host vscode.postMessage({ type: "requestIndexingStatus" }) @@ -145,6 +157,16 @@ export const CodeIndexSettings: React.FC = ({ .positive("Dimension must be a positive number") .optional(), }), + gemini: baseSchema.extend({ + codebaseIndexEmbedderProvider: z.literal("gemini"), + geminiApiKey: z.string().min(1, "Gemini API key is required"), + geminiEmbeddingTaskType: z.string().optional(), + geminiEmbeddingDimension: z + .number() + .int() + .positive("Embedding dimension must be a positive integer") + .optional(), + }), } try { @@ -153,7 +175,9 @@ export const CodeIndexSettings: React.FC = ({ ? providerSchemas.openai : config.codebaseIndexEmbedderProvider === "ollama" ? providerSchemas.ollama - : providerSchemas["openai-compatible"] + : config.codebaseIndexEmbedderProvider === "openai-compatible" + ? providerSchemas["openai-compatible"] + : providerSchemas.gemini schema.parse({ ...config, @@ -161,6 +185,7 @@ export const CodeIndexSettings: React.FC = ({ codebaseIndexOpenAiCompatibleBaseUrl: apiConfig.codebaseIndexOpenAiCompatibleBaseUrl, codebaseIndexOpenAiCompatibleApiKey: apiConfig.codebaseIndexOpenAiCompatibleApiKey, codebaseIndexOpenAiCompatibleModelDimension: apiConfig.codebaseIndexOpenAiCompatibleModelDimension, + geminiApiKey: apiConfig.geminiApiKey, }) return true } catch { @@ -275,6 +300,7 @@ export const CodeIndexSettings: React.FC = ({ {t("settings:codeIndex.openaiCompatibleProvider")} + {t("settings:codeIndex.geminiProvider")} @@ -419,6 +445,118 @@ export const CodeIndexSettings: React.FC = ({ )} + {codebaseIndexConfig?.codebaseIndexEmbedderProvider === "gemini" && ( + <> +
+
+
{t("settings:codeIndex.geminiApiKey")}
+
+
+ setApiConfigurationField("geminiApiKey", e.target.value)} + placeholder={t("settings:codeIndex.apiKeyPlaceholder")} + style={{ width: "100%" }}> +
+
+ {geminiModelProfileForDim?.supportsTaskType && ( +
+
+
{t("settings:codeIndex.embeddingTaskType")}
+
+
+
+ +
+
+
+ )} + {currentProvider === "gemini" && + geminiModelProfileForDim && + geminiSupportedDims && + geminiSupportedDims.length > 0 && ( +
+
+
{t("settings:codeIndex.embeddingDimension")}
+
+
+
+ +
+
+
+ )} +
+ setApiConfigurationField("rateLimitSeconds", value)} + /> +
+ + )} +
{t("settings:codeIndex.qdrantUrlLabel")}
diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index e25b16fd49..d3f0184a83 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -50,10 +50,25 @@ "openaiCompatibleModelDimensionLabel": "Dimensió d'Embedding:", "openaiCompatibleModelDimensionPlaceholder": "p. ex., 1536", "openaiCompatibleModelDimensionDescription": "La dimensió d'embedding (mida de sortida) per al teu model. Consulta la documentació del teu proveïdor per a aquest valor. Valors comuns: 384, 768, 1536, 3072.", + "geminiProvider": "Gemini", "openaiKeyLabel": "Clau OpenAI:", "modelLabel": "Model", "selectModelPlaceholder": "Seleccionar model", "ollamaUrlLabel": "URL d'Ollama:", + "geminiApiKey": "Clau API Gemini:", + "apiKeyPlaceholder": "Introduïu la clau API...", + "embeddingTaskType": "Tipus de tasca d'incrustació", + "selectTaskTypePlaceholder": "Seleccioneu el tipus de tasca", + "selectTaskType": { + "codeRetrievalQuery": "Consulta de recuperació de codi", + "retrievalDocument": "Document de recuperació", + "retrievalQuery": "Consulta de recuperació", + "semanticSimilarity": "Similitud semàntica", + "classification": "Classificació", + "clustering": "Agrupació" + }, + "embeddingDimension": "Dimensió d'embedding", + "selectDimensionPlaceholder": "Seleccionar dimensió", "qdrantUrlLabel": "URL de Qdrant", "qdrantKeyLabel": "Clau de Qdrant:", "startIndexingButton": "Iniciar indexació", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 3626e86cad..280a86519d 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -50,10 +50,25 @@ "openaiCompatibleModelDimensionLabel": "Embedding-Dimension:", "openaiCompatibleModelDimensionPlaceholder": "z.B. 1536", "openaiCompatibleModelDimensionDescription": "Die Embedding-Dimension (Ausgabegröße) für Ihr Modell. Überprüfen Sie die Dokumentation Ihres Anbieters für diesen Wert. Übliche Werte: 384, 768, 1536, 3072.", + "geminiProvider": "Gemini", "openaiKeyLabel": "OpenAI-Schlüssel:", "modelLabel": "Modell", "selectModelPlaceholder": "Modell auswählen", "ollamaUrlLabel": "Ollama-URL:", + "geminiApiKey": "Gemini API-Schlüssel:", + "apiKeyPlaceholder": "API-Schlüssel eingeben...", + "embeddingTaskType": "Embedding-Aufgabentyp", + "selectTaskTypePlaceholder": "Aufgabentyp auswählen", + "selectTaskType": { + "codeRetrievalQuery": "Code-Abruf-Anfrage", + "retrievalDocument": "Abrufdokument", + "retrievalQuery": "Abrufanfrage", + "semanticSimilarity": "Semantische Ähnlichkeit", + "classification": "Klassifizierung", + "clustering": "Clustering" + }, + "embeddingDimension": "Embedding-Dimension", + "selectDimensionPlaceholder": "Dimension auswählen", "qdrantUrlLabel": "Qdrant-URL", "qdrantKeyLabel": "Qdrant-Schlüssel:", "startIndexingButton": "Indexierung starten", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 011c8df2ef..777b1e09c1 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -45,6 +45,7 @@ "openaiProvider": "OpenAI", "ollamaProvider": "Ollama", "openaiCompatibleProvider": "OpenAI Compatible", + "geminiProvider": "Gemini", "openaiKeyLabel": "OpenAI Key:", "openaiCompatibleBaseUrlLabel": "Base URL:", "openaiCompatibleApiKeyLabel": "API Key:", @@ -54,6 +55,20 @@ "modelLabel": "Model", "selectModelPlaceholder": "Select model", "ollamaUrlLabel": "Ollama URL:", + "geminiApiKey": "Gemini API Key:", + "apiKeyPlaceholder": "Enter API Key...", + "embeddingTaskType": "Embedding Task Type", + "selectTaskTypePlaceholder": "Select Task Type", + "selectTaskType": { + "codeRetrievalQuery": "Code Retrieval Query", + "retrievalDocument": "Retrieval Document", + "retrievalQuery": "Retrieval Query", + "semanticSimilarity": "Semantic Similarity", + "classification": "Classification", + "clustering": "Clustering" + }, + "embeddingDimension": "Embedding Dimension", + "selectDimensionPlaceholder": "Select Dimension", "qdrantUrlLabel": "Qdrant URL", "qdrantKeyLabel": "Qdrant Key:", "startIndexingButton": "Start Indexing", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 2664e948cb..da0a1c9e2c 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -50,10 +50,25 @@ "openaiCompatibleModelDimensionLabel": "Dimensión de Embedding:", "openaiCompatibleModelDimensionPlaceholder": "ej., 1536", "openaiCompatibleModelDimensionDescription": "La dimensión de embedding (tamaño de salida) para tu modelo. Consulta la documentación de tu proveedor para este valor. Valores comunes: 384, 768, 1536, 3072.", + "geminiProvider": "Gemini", "openaiKeyLabel": "Clave de OpenAI:", "modelLabel": "Modelo", "selectModelPlaceholder": "Seleccionar modelo", "ollamaUrlLabel": "URL de Ollama:", + "geminiApiKey": "Clave API de Gemini:", + "apiKeyPlaceholder": "Introduce la clave API...", + "embeddingTaskType": "Tipo de tarea de incrustación", + "selectTaskTypePlaceholder": "Selecciona el tipo de tarea", + "selectTaskType": { + "codeRetrievalQuery": "Consulta de recuperación de código", + "retrievalDocument": "Documento de recuperación", + "retrievalQuery": "Consulta de recuperación", + "semanticSimilarity": "Similitud semántica", + "classification": "Clasificación", + "clustering": "Agrupación" + }, + "embeddingDimension": "Dimensión del embedding", + "selectDimensionPlaceholder": "Seleccionar dimensión", "qdrantUrlLabel": "URL de Qdrant", "qdrantKeyLabel": "Clave de Qdrant:", "startIndexingButton": "Iniciar indexación", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 085376b4a7..e8b6a2e00e 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -50,10 +50,25 @@ "openaiCompatibleModelDimensionLabel": "Dimension d'Embedding :", "openaiCompatibleModelDimensionPlaceholder": "ex., 1536", "openaiCompatibleModelDimensionDescription": "La dimension d'embedding (taille de sortie) pour votre modèle. Consultez la documentation de votre fournisseur pour cette valeur. Valeurs courantes : 384, 768, 1536, 3072.", + "geminiProvider": "Gemini", "openaiKeyLabel": "Clé OpenAI :", "modelLabel": "Modèle", "selectModelPlaceholder": "Sélectionner un modèle", "ollamaUrlLabel": "URL Ollama :", + "geminiApiKey": "Clé API Gemini :", + "apiKeyPlaceholder": "Saisir la clé API...", + "embeddingTaskType": "Type de tâche d'intégration", + "selectTaskTypePlaceholder": "Sélectionner le type de tâche", + "selectTaskType": { + "codeRetrievalQuery": "Requête de récupération de code", + "retrievalDocument": "Document de récupération", + "retrievalQuery": "Requête de récupération", + "semanticSimilarity": "Similarité sémantique", + "classification": "Classification", + "clustering": "Regroupement" + }, + "embeddingDimension": "Dimension d'embedding", + "selectDimensionPlaceholder": "Sélectionner la dimension", "qdrantUrlLabel": "URL Qdrant", "qdrantKeyLabel": "Clé Qdrant :", "startIndexingButton": "Démarrer l'indexation", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 62ad6caad1..a45b23416a 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -50,10 +50,25 @@ "openaiCompatibleModelDimensionLabel": "एम्बेडिंग आयाम:", "openaiCompatibleModelDimensionPlaceholder": "उदा., 1536", "openaiCompatibleModelDimensionDescription": "आपके मॉडल के लिए एम्बेडिंग आयाम (आउटपुट साइज)। इस मान के लिए अपने प्रदाता के दस्तावेज़ीकरण की जांच करें। सामान्य मान: 384, 768, 1536, 3072।", + "geminiProvider": "जेमिनी", "openaiKeyLabel": "OpenAI कुंजी:", "modelLabel": "मॉडल", "selectModelPlaceholder": "मॉडल चुनें", "ollamaUrlLabel": "Ollama URL:", + "geminiApiKey": "जेमिनी API कुंजी:", + "apiKeyPlaceholder": "API कुंजी दर्ज करें...", + "embeddingTaskType": "एम्बेडिंग कार्य प्रकार", + "selectTaskTypePlaceholder": "कार्य प्रकार चुनें", + "selectTaskType": { + "codeRetrievalQuery": "कोड पुनर्प्राप्ति क्वेरी", + "retrievalDocument": "पुनर्प्राप्ति दस्तावेज़", + "retrievalQuery": "पुनर्प्राप्ति क्वेरी", + "semanticSimilarity": "अर्थगत समानता", + "classification": "वर्गीकरण", + "clustering": "क्लस्टरिंग" + }, + "embeddingDimension": "एम्बेडिंग आयाम", + "selectDimensionPlaceholder": "आयाम चुनें", "qdrantUrlLabel": "Qdrant URL", "qdrantKeyLabel": "Qdrant कुंजी:", "startIndexingButton": "इंडेक्सिंग शुरू करें", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index afad7d8700..1b17815fda 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -45,6 +45,7 @@ "openaiProvider": "OpenAI", "ollamaProvider": "Ollama", "openaiCompatibleProvider": "OpenAI Compatible", + "geminiProvider": "Gemini", "openaiKeyLabel": "OpenAI Key:", "openaiCompatibleBaseUrlLabel": "Base URL:", "openaiCompatibleApiKeyLabel": "API Key:", @@ -54,6 +55,20 @@ "modelLabel": "Model", "selectModelPlaceholder": "Pilih model", "ollamaUrlLabel": "Ollama URL:", + "geminiApiKey": "Kunci API Gemini:", + "apiKeyPlaceholder": "Masukkan Kunci API...", + "embeddingTaskType": "Tipe Tugas Embedding", + "selectTaskTypePlaceholder": "Pilih Tipe Tugas", + "selectTaskType": { + "codeRetrievalQuery": "Kueri Pengambilan Kode", + "retrievalDocument": "Dokumen Pengambilan", + "retrievalQuery": "Kueri Pengambilan", + "semanticSimilarity": "Kemiripan Semantik", + "classification": "Klasifikasi", + "clustering": "Pengelompokan" + }, + "embeddingDimension": "Dimensi Embedding", + "selectDimensionPlaceholder": "Pilih Dimensi", "qdrantUrlLabel": "Qdrant URL", "qdrantKeyLabel": "Qdrant Key:", "startIndexingButton": "Mulai Pengindeksan", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 6d7d8417a0..cf4a52665b 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -50,10 +50,25 @@ "openaiCompatibleModelDimensionLabel": "Dimensione Embedding:", "openaiCompatibleModelDimensionPlaceholder": "es., 1536", "openaiCompatibleModelDimensionDescription": "La dimensione dell'embedding (dimensione di output) per il tuo modello. Controlla la documentazione del tuo provider per questo valore. Valori comuni: 384, 768, 1536, 3072.", + "geminiProvider": "Gemini", "openaiKeyLabel": "Chiave OpenAI:", "modelLabel": "Modello", "selectModelPlaceholder": "Seleziona modello", "ollamaUrlLabel": "URL Ollama:", + "geminiApiKey": "Chiave API Gemini:", + "apiKeyPlaceholder": "Inserisci chiave API...", + "embeddingTaskType": "Tipo di attività di embedding", + "selectTaskTypePlaceholder": "Seleziona tipo di attività", + "selectTaskType": { + "codeRetrievalQuery": "Query di recupero codice", + "retrievalDocument": "Documento di recupero", + "retrievalQuery": "Query di recupero", + "semanticSimilarity": "Similarità semantica", + "classification": "Classificazione", + "clustering": "Clustering" + }, + "embeddingDimension": "Dimensione embedding", + "selectDimensionPlaceholder": "Seleziona dimensione", "qdrantUrlLabel": "URL Qdrant", "qdrantKeyLabel": "Chiave Qdrant:", "startIndexingButton": "Avvia indicizzazione", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 14a6aae7fc..8021fb0f02 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -50,10 +50,25 @@ "openaiCompatibleModelDimensionLabel": "埋め込みディメンション:", "openaiCompatibleModelDimensionPlaceholder": "例:1536", "openaiCompatibleModelDimensionDescription": "モデルの埋め込みディメンション(出力サイズ)。この値についてはプロバイダーのドキュメントを確認してください。一般的な値:384、768、1536、3072。", + "geminiProvider": "Gemini", "openaiKeyLabel": "OpenAIキー:", "modelLabel": "モデル", "selectModelPlaceholder": "モデルを選択", "ollamaUrlLabel": "Ollama URL:", + "geminiApiKey": "Gemini APIキー:", + "apiKeyPlaceholder": "APIキーを入力...", + "embeddingTaskType": "埋め込みタスクタイプ", + "selectTaskTypePlaceholder": "タスクタイプを選択", + "selectTaskType": { + "codeRetrievalQuery": "コード検索クエリ", + "retrievalDocument": "検索ドキュメント", + "retrievalQuery": "検索クエリ", + "semanticSimilarity": "セマンティック類似性", + "classification": "分類", + "clustering": "クラスタリング" + }, + "embeddingDimension": "埋め込み次元", + "selectDimensionPlaceholder": "次元を選択", "qdrantUrlLabel": "Qdrant URL", "qdrantKeyLabel": "Qdrantキー:", "startIndexingButton": "インデックス作成を開始", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index abfd71bf36..97df1d0fcc 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -50,10 +50,25 @@ "openaiCompatibleModelDimensionLabel": "임베딩 차원:", "openaiCompatibleModelDimensionPlaceholder": "예: 1536", "openaiCompatibleModelDimensionDescription": "모델의 임베딩 차원(출력 크기)입니다. 이 값에 대해서는 제공업체의 문서를 확인하세요. 일반적인 값: 384, 768, 1536, 3072.", + "geminiProvider": "Gemini", "openaiKeyLabel": "OpenAI 키:", "modelLabel": "모델", "selectModelPlaceholder": "모델 선택", "ollamaUrlLabel": "Ollama URL:", + "geminiApiKey": "Gemini API 키:", + "apiKeyPlaceholder": "API 키 입력...", + "embeddingTaskType": "임베딩 작업 유형", + "selectTaskTypePlaceholder": "작업 유형 선택", + "selectTaskType": { + "codeRetrievalQuery": "코드 검색 쿼리", + "retrievalDocument": "검색 문서", + "retrievalQuery": "검색 쿼리", + "semanticSimilarity": "의미론적 유사성", + "classification": "분류", + "clustering": "클러스터링" + }, + "embeddingDimension": "임베딩 차원", + "selectDimensionPlaceholder": "차원 선택", "qdrantUrlLabel": "Qdrant URL", "qdrantKeyLabel": "Qdrant 키:", "startIndexingButton": "인덱싱 시작", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 54b5c32795..231c26306f 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -50,11 +50,26 @@ "openaiCompatibleModelDimensionLabel": "Embedding Dimensie:", "openaiCompatibleModelDimensionPlaceholder": "bijv., 1536", "openaiCompatibleModelDimensionDescription": "De embedding dimensie (uitvoergrootte) voor uw model. Controleer de documentatie van uw provider voor deze waarde. Veelvoorkomende waarden: 384, 768, 1536, 3072.", + "geminiProvider": "Gemini", "openaiKeyLabel": "OpenAI-sleutel:", "modelLabel": "Model", "selectModelPlaceholder": "Selecteer model", - "ollamaUrlLabel": "Ollama URL:", - "qdrantUrlLabel": "Qdrant URL", + "ollamaUrlLabel": "Ollama-URL:", + "geminiApiKey": "Gemini API-sleutel:", + "apiKeyPlaceholder": "Voer API-sleutel in...", + "embeddingTaskType": "Type embeddingtaak", + "selectTaskTypePlaceholder": "Selecteer taaktype", + "selectTaskType": { + "codeRetrievalQuery": "Code ophaalquery", + "retrievalDocument": "Ophaaldocument", + "retrievalQuery": "Ophaalquery", + "semanticSimilarity": "Semantische overeenkomst", + "classification": "Classificatie", + "clustering": "Clustering" + }, + "embeddingDimension": "Embedding dimensie", + "selectDimensionPlaceholder": "Selecteer dimensie", + "qdrantUrlLabel": "Qdrant-URL", "qdrantKeyLabel": "Qdrant-sleutel:", "startIndexingButton": "Indexering starten", "clearIndexDataButton": "Indexgegevens wissen", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 96e33e60b9..062bab02ec 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -50,10 +50,25 @@ "openaiCompatibleModelDimensionLabel": "Wymiar Embeddingu:", "openaiCompatibleModelDimensionPlaceholder": "np., 1536", "openaiCompatibleModelDimensionDescription": "Wymiar embeddingu (rozmiar wyjściowy) dla twojego modelu. Sprawdź dokumentację swojego dostawcy, aby uzyskać tę wartość. Typowe wartości: 384, 768, 1536, 3072.", + "geminiProvider": "Gemini", "openaiKeyLabel": "Klucz OpenAI:", "modelLabel": "Model", "selectModelPlaceholder": "Wybierz model", "ollamaUrlLabel": "URL Ollama:", + "geminiApiKey": "Klucz API Gemini:", + "apiKeyPlaceholder": "Wprowadź klucz API...", + "embeddingTaskType": "Typ zadania osadzania", + "selectTaskTypePlaceholder": "Wybierz typ zadania", + "selectTaskType": { + "codeRetrievalQuery": "Zapytanie o odzyskanie kodu", + "retrievalDocument": "Dokument odzyskiwania", + "retrievalQuery": "Zapytanie o odzyskanie", + "semanticSimilarity": "Podobieństwo semantyczne", + "classification": "Klasyfikacja", + "clustering": "Klastrowanie" + }, + "embeddingDimension": "Wymiar osadzania", + "selectDimensionPlaceholder": "Wybierz wymiar", "qdrantUrlLabel": "URL Qdrant", "qdrantKeyLabel": "Klucz Qdrant:", "startIndexingButton": "Rozpocznij indeksowanie", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 56b82cc849..afa6bb27da 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -50,11 +50,26 @@ "openaiCompatibleModelDimensionLabel": "Dimensão de Embedding:", "openaiCompatibleModelDimensionPlaceholder": "ex., 1536", "openaiCompatibleModelDimensionDescription": "A dimensão de embedding (tamanho de saída) para seu modelo. Verifique a documentação do seu provedor para este valor. Valores comuns: 384, 768, 1536, 3072.", + "geminiProvider": "Gemini", "openaiKeyLabel": "Chave OpenAI:", "modelLabel": "Modelo", "selectModelPlaceholder": "Selecionar modelo", - "ollamaUrlLabel": "URL Ollama:", - "qdrantUrlLabel": "URL Qdrant", + "ollamaUrlLabel": "URL do Ollama:", + "geminiApiKey": "Chave da API Gemini:", + "apiKeyPlaceholder": "Insira a chave da API...", + "embeddingTaskType": "Tipo de Tarefa de Embedding", + "selectTaskTypePlaceholder": "Selecione o Tipo de Tarefa", + "selectTaskType": { + "codeRetrievalQuery": "Consulta de Recuperação de Código", + "retrievalDocument": "Documento de Recuperação", + "retrievalQuery": "Consulta de Recuperação", + "semanticSimilarity": "Similaridade Semântica", + "classification": "Classificação", + "clustering": "Agrupamento" + }, + "embeddingDimension": "Dimensão do embedding", + "selectDimensionPlaceholder": "Selecionar dimensão", + "qdrantUrlLabel": "URL do Qdrant", "qdrantKeyLabel": "Chave Qdrant:", "startIndexingButton": "Iniciar Indexação", "clearIndexDataButton": "Limpar Dados de Índice", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 6290cbe238..88e38974b9 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -50,10 +50,25 @@ "openaiCompatibleModelDimensionLabel": "Размерность эмбеддинга:", "openaiCompatibleModelDimensionPlaceholder": "напр., 1536", "openaiCompatibleModelDimensionDescription": "Размерность эмбеддинга (размер выходных данных) для вашей модели. Проверьте документацию вашего провайдера для этого значения. Распространенные значения: 384, 768, 1536, 3072.", + "geminiProvider": "Gemini", "openaiKeyLabel": "Ключ OpenAI:", "modelLabel": "Модель", "selectModelPlaceholder": "Выберите модель", "ollamaUrlLabel": "URL Ollama:", + "geminiApiKey": "Ключ API Gemini:", + "apiKeyPlaceholder": "Введите ключ API...", + "embeddingTaskType": "Тип задачи встраивания", + "selectTaskTypePlaceholder": "Выберите тип задачи", + "selectTaskType": { + "codeRetrievalQuery": "Запрос на поиск кода", + "retrievalDocument": "Документ для поиска", + "retrievalQuery": "Поисковый запрос", + "semanticSimilarity": "Семантическое сходство", + "classification": "Классификация", + "clustering": "Кластеризация" + }, + "embeddingDimension": "Размерность эмбеддинга", + "selectDimensionPlaceholder": "Выберите размерность", "qdrantUrlLabel": "URL Qdrant", "qdrantKeyLabel": "Ключ Qdrant:", "startIndexingButton": "Начать индексацию", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index ab4992d4cb..70e567bdd6 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -50,11 +50,26 @@ "openaiCompatibleModelDimensionLabel": "Gömme Boyutu:", "openaiCompatibleModelDimensionPlaceholder": "örn., 1536", "openaiCompatibleModelDimensionDescription": "Modeliniz için gömme boyutu (çıktı boyutu). Bu değer için sağlayıcınızın belgelerine bakın. Yaygın değerler: 384, 768, 1536, 3072.", + "geminiProvider": "Gemini", "openaiKeyLabel": "OpenAI Anahtarı:", "modelLabel": "Model", "selectModelPlaceholder": "Model seç", - "ollamaUrlLabel": "Ollama URL:", - "qdrantUrlLabel": "Qdrant URL", + "ollamaUrlLabel": "Ollama URL'si:", + "geminiApiKey": "Gemini API Anahtarı:", + "apiKeyPlaceholder": "API Anahtarını Girin...", + "embeddingTaskType": "Gömme Görev Türü", + "selectTaskTypePlaceholder": "Görev Türü Seçin", + "selectTaskType": { + "codeRetrievalQuery": "Kod Alma Sorgusu", + "retrievalDocument": "Alma Belgesi", + "retrievalQuery": "Alma Sorgusu", + "semanticSimilarity": "Anlamsal Benzerlik", + "classification": "Sınıflandırma", + "clustering": "Kümeleme" + }, + "embeddingDimension": "Gömme Boyutu", + "selectDimensionPlaceholder": "Boyut Seçin", + "qdrantUrlLabel": "Qdrant URL'si", "qdrantKeyLabel": "Qdrant Anahtarı:", "startIndexingButton": "İndekslemeyi Başlat", "clearIndexDataButton": "İndeks Verilerini Temizle", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 9656e0c6e1..18254e913d 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -50,10 +50,25 @@ "openaiCompatibleModelDimensionLabel": "Kích thước Embedding:", "openaiCompatibleModelDimensionPlaceholder": "vd., 1536", "openaiCompatibleModelDimensionDescription": "Kích thước embedding (kích thước đầu ra) cho mô hình của bạn. Kiểm tra tài liệu của nhà cung cấp để biết giá trị này. Giá trị phổ biến: 384, 768, 1536, 3072.", + "geminiProvider": "Gemini", "openaiKeyLabel": "Khóa OpenAI:", "modelLabel": "Mô hình", "selectModelPlaceholder": "Chọn mô hình", "ollamaUrlLabel": "URL Ollama:", + "geminiApiKey": "Khóa API Gemini:", + "apiKeyPlaceholder": "Nhập khóa API...", + "embeddingTaskType": "Loại tác vụ nhúng", + "selectTaskTypePlaceholder": "Chọn loại tác vụ", + "selectTaskType": { + "codeRetrievalQuery": "Truy vấn truy xuất mã", + "retrievalDocument": "Tài liệu truy xuất", + "retrievalQuery": "Truy vấn truy xuất", + "semanticSimilarity": "Tương đồng ngữ nghĩa", + "classification": "Phân loại", + "clustering": "Phân cụm" + }, + "embeddingDimension": "Kích thước nhúng", + "selectDimensionPlaceholder": "Chọn kích thước", "qdrantUrlLabel": "URL Qdrant", "qdrantKeyLabel": "Khóa Qdrant:", "startIndexingButton": "Bắt đầu lập chỉ mục", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index c163920021..c5f95a52e8 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -50,10 +50,25 @@ "openaiCompatibleModelDimensionLabel": "嵌入维度:", "openaiCompatibleModelDimensionPlaceholder": "例如,1536", "openaiCompatibleModelDimensionDescription": "模型的嵌入维度(输出大小)。请查阅您的提供商文档获取此值。常见值:384、768、1536、3072。", + "geminiProvider": "Gemini", "openaiKeyLabel": "OpenAI 密钥:", "modelLabel": "模型", "selectModelPlaceholder": "选择模型", "ollamaUrlLabel": "Ollama URL:", + "geminiApiKey": "Gemini API 密钥:", + "apiKeyPlaceholder": "输入 API 密钥...", + "embeddingTaskType": "嵌入任务类型", + "selectTaskTypePlaceholder": "选择任务类型", + "selectTaskType": { + "codeRetrievalQuery": "代码检索查询", + "retrievalDocument": "检索文档", + "retrievalQuery": "检索查询", + "semanticSimilarity": "语义相似度", + "classification": "分类", + "clustering": "聚类" + }, + "embeddingDimension": "嵌入维度", + "selectDimensionPlaceholder": "选择维度", "qdrantUrlLabel": "Qdrant URL", "qdrantKeyLabel": "Qdrant 密钥:", "startIndexingButton": "开始索引", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index e85da857d0..a9098c051c 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -50,10 +50,25 @@ "openaiCompatibleModelDimensionLabel": "嵌入維度:", "openaiCompatibleModelDimensionPlaceholder": "例如,1536", "openaiCompatibleModelDimensionDescription": "模型的嵌入維度(輸出大小)。請查閱您的提供商文件獲取此值。常見值:384、768、1536、3072。", + "geminiProvider": "Gemini", "openaiKeyLabel": "OpenAI 金鑰:", "modelLabel": "模型", "selectModelPlaceholder": "選擇模型", "ollamaUrlLabel": "Ollama URL:", + "geminiApiKey": "Gemini API 金鑰:", + "apiKeyPlaceholder": "輸入 API 金鑰...", + "embeddingTaskType": "嵌入任務類型", + "selectTaskTypePlaceholder": "選取任務類型", + "selectTaskType": { + "codeRetrievalQuery": "程式碼擷取查詢", + "retrievalDocument": "擷取文件", + "retrievalQuery": "擷取查詢", + "semanticSimilarity": "語意相似度", + "classification": "分類", + "clustering": "分群" + }, + "embeddingDimension": "嵌入維度", + "selectDimensionPlaceholder": "選取維度", "qdrantUrlLabel": "Qdrant URL", "qdrantKeyLabel": "Qdrant 金鑰:", "startIndexingButton": "開始索引",