feat: add OpenRouter as an embedding provider for code indexing

- Add OpenRouter to EmbedderProvider type and related interfaces
- Create OpenRouterEmbedder class that extends OpenAICompatibleEmbedder
- Add OpenRouter embedding models to EMBEDDING_MODEL_PROFILES
- Update service factory to handle OpenRouter embedder creation
- Configure OpenRouter to use existing API key from chat model settings
- Support OpenRouter embedding models including OpenAI, Cohere, and Voyage models

Closes #8972
This commit is contained in:
Roo Code 2025-11-01 21:56:35 +00:00
parent d7ea6d679e
commit c162cf6922
8 changed files with 134 additions and 3 deletions

View file

@ -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,

View file

@ -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",
}
}
}

View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -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(

View file

@ -292,6 +292,7 @@ export interface WebviewMessage {
| "gemini"
| "mistral"
| "vercel-ai-gateway"
| "openrouter"
codebaseIndexEmbedderBaseUrl?: string
codebaseIndexEmbedderModelId: string
codebaseIndexEmbedderModelDimension?: number // Generic dimension for all providers

View file

@ -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.`)