diff --git a/src/services/code-index/config-manager.ts b/src/services/code-index/config-manager.ts index 2c0e8bb5c9..6505a7436b 100644 --- a/src/services/code-index/config-manager.ts +++ b/src/services/code-index/config-manager.ts @@ -20,6 +20,7 @@ export class CodeIndexConfigManager { private geminiOptions?: { apiKey: string } private mistralOptions?: { apiKey: string } private vercelAiGatewayOptions?: { apiKey: string } + private openRouterOptions?: { apiKey: string; baseUrl?: string } private qdrantUrl?: string = "http://localhost:6333" private qdrantApiKey?: string private searchMinScore?: number @@ -71,6 +72,9 @@ export class CodeIndexConfigManager { const geminiApiKey = this.contextProxy?.getSecret("codebaseIndexGeminiApiKey") ?? "" const mistralApiKey = this.contextProxy?.getSecret("codebaseIndexMistralApiKey") ?? "" const vercelAiGatewayApiKey = this.contextProxy?.getSecret("codebaseIndexVercelAiGatewayApiKey") ?? "" + // Use existing openRouterApiKey from chat model settings for embeddings + const openRouterApiKey = this.contextProxy?.getSecret("openRouterApiKey") ?? "" + const openRouterBaseUrl = codebaseIndexConfig.codebaseIndexEmbedderBaseUrl ?? "" // Update instance variables with configuration this.codebaseIndexEnabled = codebaseIndexEnabled ?? true @@ -108,6 +112,8 @@ export class CodeIndexConfigManager { this.embedderProvider = "mistral" } else if (codebaseIndexEmbedderProvider === "vercel-ai-gateway") { this.embedderProvider = "vercel-ai-gateway" + } else if (codebaseIndexEmbedderProvider === "openrouter") { + this.embedderProvider = "openrouter" } else { this.embedderProvider = "openai" } @@ -129,6 +135,9 @@ export class CodeIndexConfigManager { this.geminiOptions = geminiApiKey ? { apiKey: geminiApiKey } : undefined this.mistralOptions = mistralApiKey ? { apiKey: mistralApiKey } : undefined this.vercelAiGatewayOptions = vercelAiGatewayApiKey ? { apiKey: vercelAiGatewayApiKey } : undefined + this.openRouterOptions = openRouterApiKey + ? { apiKey: openRouterApiKey, baseUrl: openRouterBaseUrl || undefined } + : undefined } /** @@ -147,6 +156,7 @@ export class CodeIndexConfigManager { geminiOptions?: { apiKey: string } mistralOptions?: { apiKey: string } vercelAiGatewayOptions?: { apiKey: string } + openRouterOptions?: { apiKey: string; baseUrl?: string } qdrantUrl?: string qdrantApiKey?: string searchMinScore?: number @@ -167,6 +177,8 @@ export class CodeIndexConfigManager { geminiApiKey: this.geminiOptions?.apiKey ?? "", mistralApiKey: this.mistralOptions?.apiKey ?? "", vercelAiGatewayApiKey: this.vercelAiGatewayOptions?.apiKey ?? "", + openRouterApiKey: this.openRouterOptions?.apiKey ?? "", + openRouterBaseUrl: this.openRouterOptions?.baseUrl ?? "", qdrantUrl: this.qdrantUrl ?? "", qdrantApiKey: this.qdrantApiKey ?? "", } @@ -192,6 +204,7 @@ export class CodeIndexConfigManager { geminiOptions: this.geminiOptions, mistralOptions: this.mistralOptions, vercelAiGatewayOptions: this.vercelAiGatewayOptions, + openRouterOptions: this.openRouterOptions, qdrantUrl: this.qdrantUrl, qdrantApiKey: this.qdrantApiKey, searchMinScore: this.currentSearchMinScore, @@ -234,6 +247,11 @@ export class CodeIndexConfigManager { const qdrantUrl = this.qdrantUrl const isConfigured = !!(apiKey && qdrantUrl) return isConfigured + } else if (this.embedderProvider === "openrouter") { + const apiKey = this.openRouterOptions?.apiKey + const qdrantUrl = this.qdrantUrl + const isConfigured = !!(apiKey && qdrantUrl) + return isConfigured } return false // Should not happen if embedderProvider is always set correctly } @@ -269,6 +287,8 @@ export class CodeIndexConfigManager { const prevGeminiApiKey = prev?.geminiApiKey ?? "" const prevMistralApiKey = prev?.mistralApiKey ?? "" const prevVercelAiGatewayApiKey = prev?.vercelAiGatewayApiKey ?? "" + const prevOpenRouterApiKey = prev?.openRouterApiKey ?? "" + const prevOpenRouterBaseUrl = prev?.openRouterBaseUrl ?? "" const prevQdrantUrl = prev?.qdrantUrl ?? "" const prevQdrantApiKey = prev?.qdrantApiKey ?? "" @@ -307,6 +327,8 @@ export class CodeIndexConfigManager { const currentGeminiApiKey = this.geminiOptions?.apiKey ?? "" const currentMistralApiKey = this.mistralOptions?.apiKey ?? "" const currentVercelAiGatewayApiKey = this.vercelAiGatewayOptions?.apiKey ?? "" + const currentOpenRouterApiKey = this.openRouterOptions?.apiKey ?? "" + const currentOpenRouterBaseUrl = this.openRouterOptions?.baseUrl ?? "" const currentQdrantUrl = this.qdrantUrl ?? "" const currentQdrantApiKey = this.qdrantApiKey ?? "" @@ -337,6 +359,10 @@ export class CodeIndexConfigManager { return true } + if (prevOpenRouterApiKey !== currentOpenRouterApiKey || prevOpenRouterBaseUrl !== currentOpenRouterBaseUrl) { + return true + } + // Check for model dimension changes (generic for all providers) if (prevModelDimension !== currentModelDimension) { return true @@ -395,6 +421,7 @@ export class CodeIndexConfigManager { geminiOptions: this.geminiOptions, mistralOptions: this.mistralOptions, vercelAiGatewayOptions: this.vercelAiGatewayOptions, + openRouterOptions: this.openRouterOptions, qdrantUrl: this.qdrantUrl, qdrantApiKey: this.qdrantApiKey, searchMinScore: this.currentSearchMinScore, diff --git a/src/services/code-index/embedders/openrouter.ts b/src/services/code-index/embedders/openrouter.ts new file mode 100644 index 0000000000..5976ea2dd3 --- /dev/null +++ b/src/services/code-index/embedders/openrouter.ts @@ -0,0 +1,53 @@ +import { OpenAICompatibleEmbedder } from "./openai-compatible" +import { IEmbedder, EmbedderInfo } from "../interfaces/embedder" +import { getDefaultModelId } from "../../../shared/embeddingModels" +import { MAX_ITEM_TOKENS } from "../constants" +import { t } from "../../../i18n" + +/** + * OpenRouter embedder implementation that wraps the OpenAI Compatible embedder + * with configuration for OpenRouter's embedding API. + * + * Supported models: + * - openai/text-embedding-3-small (dimension: 1536) + * - openai/text-embedding-3-large (dimension: 3072) + * - openai/text-embedding-ada-002 (dimension: 1536) + * - cohere/embed-english-v3.0 (dimension: 1024) + * - cohere/embed-multilingual-v3.0 (dimension: 1024) + * - voyage/voyage-3 (dimension: 1024) + * - voyage/voyage-3-lite (dimension: 512) + */ +export class OpenRouterEmbedder extends OpenAICompatibleEmbedder implements IEmbedder { + private static readonly OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1" + private static readonly DEFAULT_MODEL = "openai/text-embedding-3-small" + private readonly modelId: string + + /** + * Creates a new OpenRouter embedder + * @param apiKey The OpenRouter API key for authentication + * @param modelId The model ID to use (defaults to openai/text-embedding-3-small) + * @param baseUrl Optional custom base URL for OpenRouter API (defaults to https://openrouter.ai/api/v1) + */ + constructor(apiKey?: string, modelId?: string, baseUrl?: string) { + if (!apiKey) { + throw new Error(t("embeddings:validation.apiKeyRequired")) + } + + // Use the provided base URL or default to OpenRouter's API URL + const openRouterBaseUrl = baseUrl || OpenRouterEmbedder.OPENROUTER_BASE_URL + + // Initialize the parent OpenAI Compatible embedder with OpenRouter configuration + super(openRouterBaseUrl, apiKey, modelId || OpenRouterEmbedder.DEFAULT_MODEL, MAX_ITEM_TOKENS) + + this.modelId = modelId || getDefaultModelId("openrouter") + } + + /** + * Returns information about this embedder + */ + override get embedderInfo(): EmbedderInfo { + return { + name: "openrouter", + } + } +} diff --git a/src/services/code-index/interfaces/config.ts b/src/services/code-index/interfaces/config.ts index f168e26869..2ddbd35d9e 100644 --- a/src/services/code-index/interfaces/config.ts +++ b/src/services/code-index/interfaces/config.ts @@ -15,6 +15,7 @@ export interface CodeIndexConfig { geminiOptions?: { apiKey: string } mistralOptions?: { apiKey: string } vercelAiGatewayOptions?: { apiKey: string } + openRouterOptions?: { apiKey: string; baseUrl?: string } qdrantUrl?: string qdrantApiKey?: string searchMinScore?: number @@ -37,6 +38,8 @@ export type PreviousConfigSnapshot = { geminiApiKey?: string mistralApiKey?: string vercelAiGatewayApiKey?: string + openRouterApiKey?: string + openRouterBaseUrl?: string qdrantUrl?: string qdrantApiKey?: string } diff --git a/src/services/code-index/interfaces/embedder.ts b/src/services/code-index/interfaces/embedder.ts index 1fcda3aca3..7a3aa91ad9 100644 --- a/src/services/code-index/interfaces/embedder.ts +++ b/src/services/code-index/interfaces/embedder.ts @@ -28,7 +28,14 @@ export interface EmbeddingResponse { } } -export type AvailableEmbedders = "openai" | "ollama" | "openai-compatible" | "gemini" | "mistral" | "vercel-ai-gateway" +export type AvailableEmbedders = + | "openai" + | "ollama" + | "openai-compatible" + | "gemini" + | "mistral" + | "vercel-ai-gateway" + | "openrouter" export interface EmbedderInfo { name: AvailableEmbedders diff --git a/src/services/code-index/interfaces/manager.ts b/src/services/code-index/interfaces/manager.ts index 527900f6d1..9a6e4031ab 100644 --- a/src/services/code-index/interfaces/manager.ts +++ b/src/services/code-index/interfaces/manager.ts @@ -70,7 +70,14 @@ export interface ICodeIndexManager { } export type IndexingState = "Standby" | "Indexing" | "Indexed" | "Error" -export type EmbedderProvider = "openai" | "ollama" | "openai-compatible" | "gemini" | "mistral" | "vercel-ai-gateway" +export type EmbedderProvider = + | "openai" + | "ollama" + | "openai-compatible" + | "gemini" + | "mistral" + | "vercel-ai-gateway" + | "openrouter" export interface IndexProgressUpdate { systemStatus: IndexingState diff --git a/src/services/code-index/service-factory.ts b/src/services/code-index/service-factory.ts index 6d69e1f0b6..bcffd9732a 100644 --- a/src/services/code-index/service-factory.ts +++ b/src/services/code-index/service-factory.ts @@ -5,6 +5,7 @@ import { OpenAICompatibleEmbedder } from "./embedders/openai-compatible" import { GeminiEmbedder } from "./embedders/gemini" import { MistralEmbedder } from "./embedders/mistral" import { VercelAiGatewayEmbedder } from "./embedders/vercel-ai-gateway" +import { OpenRouterEmbedder } from "./embedders/openrouter" import { EmbedderProvider, getDefaultModelId, getModelDimension } from "../../shared/embeddingModels" import { QdrantVectorStore } from "./vector-store/qdrant-client" import { codeParser, DirectoryScanner, FileWatcher } from "./processors" @@ -79,6 +80,15 @@ export class CodeIndexServiceFactory { throw new Error(t("embeddings:serviceFactory.vercelAiGatewayConfigMissing")) } return new VercelAiGatewayEmbedder(config.vercelAiGatewayOptions.apiKey, config.modelId) + } else if (provider === "openrouter") { + if (!config.openRouterOptions?.apiKey) { + throw new Error(t("embeddings:serviceFactory.openRouterConfigMissing")) + } + return new OpenRouterEmbedder( + config.openRouterOptions.apiKey, + config.modelId, + config.openRouterOptions.baseUrl, + ) } throw new Error( diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 9c47518628..7f3280bb23 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -292,6 +292,7 @@ export interface WebviewMessage { | "gemini" | "mistral" | "vercel-ai-gateway" + | "openrouter" codebaseIndexEmbedderBaseUrl?: string codebaseIndexEmbedderModelId: string codebaseIndexEmbedderModelDimension?: number // Generic dimension for all providers diff --git a/src/shared/embeddingModels.ts b/src/shared/embeddingModels.ts index 80c51a6b45..a8655df726 100644 --- a/src/shared/embeddingModels.ts +++ b/src/shared/embeddingModels.ts @@ -2,7 +2,14 @@ * Defines profiles for different embedding models, including their dimensions. */ -export type EmbedderProvider = "openai" | "ollama" | "openai-compatible" | "gemini" | "mistral" | "vercel-ai-gateway" // Add other providers as needed +export type EmbedderProvider = + | "openai" + | "ollama" + | "openai-compatible" + | "gemini" + | "mistral" + | "vercel-ai-gateway" + | "openrouter" // Add other providers as needed export interface EmbeddingModelProfile { dimension: number @@ -70,6 +77,19 @@ export const EMBEDDING_MODEL_PROFILES: EmbeddingModelProfiles = { "mistral/codestral-embed": { dimension: 1536, scoreThreshold: 0.4 }, "mistral/mistral-embed": { dimension: 1024, scoreThreshold: 0.4 }, }, + openrouter: { + // OpenAI models available via OpenRouter + "openai/text-embedding-3-small": { dimension: 1536, scoreThreshold: 0.4 }, + "openai/text-embedding-3-large": { dimension: 3072, scoreThreshold: 0.4 }, + "openai/text-embedding-ada-002": { dimension: 1536, scoreThreshold: 0.4 }, + // Cohere models + "cohere/embed-english-v3.0": { dimension: 1024, scoreThreshold: 0.4 }, + "cohere/embed-multilingual-v3.0": { dimension: 1024, scoreThreshold: 0.4 }, + // Voyage models + "voyage/voyage-embeddings-v3": { dimension: 1024, scoreThreshold: 0.4 }, + "voyage/voyage-3": { dimension: 1024, scoreThreshold: 0.4 }, + "voyage/voyage-3-lite": { dimension: 512, scoreThreshold: 0.4 }, + }, } /** @@ -163,6 +183,9 @@ export function getDefaultModelId(provider: EmbedderProvider): string { case "vercel-ai-gateway": return "openai/text-embedding-3-large" + case "openrouter": + return "openai/text-embedding-3-small" + default: // Fallback for unknown providers console.warn(`Unknown provider for default model ID: ${provider}. Falling back to OpenAI default.`)