Skip to content

Commit c162cf6

Browse files
committed
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
1 parent d7ea6d6 commit c162cf6

File tree

8 files changed

+134
-3
lines changed

8 files changed

+134
-3
lines changed

src/services/code-index/config-manager.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export class CodeIndexConfigManager {
2020
private geminiOptions?: { apiKey: string }
2121
private mistralOptions?: { apiKey: string }
2222
private vercelAiGatewayOptions?: { apiKey: string }
23+
private openRouterOptions?: { apiKey: string; baseUrl?: string }
2324
private qdrantUrl?: string = "http://localhost:6333"
2425
private qdrantApiKey?: string
2526
private searchMinScore?: number
@@ -71,6 +72,9 @@ export class CodeIndexConfigManager {
7172
const geminiApiKey = this.contextProxy?.getSecret("codebaseIndexGeminiApiKey") ?? ""
7273
const mistralApiKey = this.contextProxy?.getSecret("codebaseIndexMistralApiKey") ?? ""
7374
const vercelAiGatewayApiKey = this.contextProxy?.getSecret("codebaseIndexVercelAiGatewayApiKey") ?? ""
75+
// Use existing openRouterApiKey from chat model settings for embeddings
76+
const openRouterApiKey = this.contextProxy?.getSecret("openRouterApiKey") ?? ""
77+
const openRouterBaseUrl = codebaseIndexConfig.codebaseIndexEmbedderBaseUrl ?? ""
7478

7579
// Update instance variables with configuration
7680
this.codebaseIndexEnabled = codebaseIndexEnabled ?? true
@@ -108,6 +112,8 @@ export class CodeIndexConfigManager {
108112
this.embedderProvider = "mistral"
109113
} else if (codebaseIndexEmbedderProvider === "vercel-ai-gateway") {
110114
this.embedderProvider = "vercel-ai-gateway"
115+
} else if (codebaseIndexEmbedderProvider === "openrouter") {
116+
this.embedderProvider = "openrouter"
111117
} else {
112118
this.embedderProvider = "openai"
113119
}
@@ -129,6 +135,9 @@ export class CodeIndexConfigManager {
129135
this.geminiOptions = geminiApiKey ? { apiKey: geminiApiKey } : undefined
130136
this.mistralOptions = mistralApiKey ? { apiKey: mistralApiKey } : undefined
131137
this.vercelAiGatewayOptions = vercelAiGatewayApiKey ? { apiKey: vercelAiGatewayApiKey } : undefined
138+
this.openRouterOptions = openRouterApiKey
139+
? { apiKey: openRouterApiKey, baseUrl: openRouterBaseUrl || undefined }
140+
: undefined
132141
}
133142

134143
/**
@@ -147,6 +156,7 @@ export class CodeIndexConfigManager {
147156
geminiOptions?: { apiKey: string }
148157
mistralOptions?: { apiKey: string }
149158
vercelAiGatewayOptions?: { apiKey: string }
159+
openRouterOptions?: { apiKey: string; baseUrl?: string }
150160
qdrantUrl?: string
151161
qdrantApiKey?: string
152162
searchMinScore?: number
@@ -167,6 +177,8 @@ export class CodeIndexConfigManager {
167177
geminiApiKey: this.geminiOptions?.apiKey ?? "",
168178
mistralApiKey: this.mistralOptions?.apiKey ?? "",
169179
vercelAiGatewayApiKey: this.vercelAiGatewayOptions?.apiKey ?? "",
180+
openRouterApiKey: this.openRouterOptions?.apiKey ?? "",
181+
openRouterBaseUrl: this.openRouterOptions?.baseUrl ?? "",
170182
qdrantUrl: this.qdrantUrl ?? "",
171183
qdrantApiKey: this.qdrantApiKey ?? "",
172184
}
@@ -192,6 +204,7 @@ export class CodeIndexConfigManager {
192204
geminiOptions: this.geminiOptions,
193205
mistralOptions: this.mistralOptions,
194206
vercelAiGatewayOptions: this.vercelAiGatewayOptions,
207+
openRouterOptions: this.openRouterOptions,
195208
qdrantUrl: this.qdrantUrl,
196209
qdrantApiKey: this.qdrantApiKey,
197210
searchMinScore: this.currentSearchMinScore,
@@ -234,6 +247,11 @@ export class CodeIndexConfigManager {
234247
const qdrantUrl = this.qdrantUrl
235248
const isConfigured = !!(apiKey && qdrantUrl)
236249
return isConfigured
250+
} else if (this.embedderProvider === "openrouter") {
251+
const apiKey = this.openRouterOptions?.apiKey
252+
const qdrantUrl = this.qdrantUrl
253+
const isConfigured = !!(apiKey && qdrantUrl)
254+
return isConfigured
237255
}
238256
return false // Should not happen if embedderProvider is always set correctly
239257
}
@@ -269,6 +287,8 @@ export class CodeIndexConfigManager {
269287
const prevGeminiApiKey = prev?.geminiApiKey ?? ""
270288
const prevMistralApiKey = prev?.mistralApiKey ?? ""
271289
const prevVercelAiGatewayApiKey = prev?.vercelAiGatewayApiKey ?? ""
290+
const prevOpenRouterApiKey = prev?.openRouterApiKey ?? ""
291+
const prevOpenRouterBaseUrl = prev?.openRouterBaseUrl ?? ""
272292
const prevQdrantUrl = prev?.qdrantUrl ?? ""
273293
const prevQdrantApiKey = prev?.qdrantApiKey ?? ""
274294

@@ -307,6 +327,8 @@ export class CodeIndexConfigManager {
307327
const currentGeminiApiKey = this.geminiOptions?.apiKey ?? ""
308328
const currentMistralApiKey = this.mistralOptions?.apiKey ?? ""
309329
const currentVercelAiGatewayApiKey = this.vercelAiGatewayOptions?.apiKey ?? ""
330+
const currentOpenRouterApiKey = this.openRouterOptions?.apiKey ?? ""
331+
const currentOpenRouterBaseUrl = this.openRouterOptions?.baseUrl ?? ""
310332
const currentQdrantUrl = this.qdrantUrl ?? ""
311333
const currentQdrantApiKey = this.qdrantApiKey ?? ""
312334

@@ -337,6 +359,10 @@ export class CodeIndexConfigManager {
337359
return true
338360
}
339361

362+
if (prevOpenRouterApiKey !== currentOpenRouterApiKey || prevOpenRouterBaseUrl !== currentOpenRouterBaseUrl) {
363+
return true
364+
}
365+
340366
// Check for model dimension changes (generic for all providers)
341367
if (prevModelDimension !== currentModelDimension) {
342368
return true
@@ -395,6 +421,7 @@ export class CodeIndexConfigManager {
395421
geminiOptions: this.geminiOptions,
396422
mistralOptions: this.mistralOptions,
397423
vercelAiGatewayOptions: this.vercelAiGatewayOptions,
424+
openRouterOptions: this.openRouterOptions,
398425
qdrantUrl: this.qdrantUrl,
399426
qdrantApiKey: this.qdrantApiKey,
400427
searchMinScore: this.currentSearchMinScore,
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { OpenAICompatibleEmbedder } from "./openai-compatible"
2+
import { IEmbedder, EmbedderInfo } from "../interfaces/embedder"
3+
import { getDefaultModelId } from "../../../shared/embeddingModels"
4+
import { MAX_ITEM_TOKENS } from "../constants"
5+
import { t } from "../../../i18n"
6+
7+
/**
8+
* OpenRouter embedder implementation that wraps the OpenAI Compatible embedder
9+
* with configuration for OpenRouter's embedding API.
10+
*
11+
* Supported models:
12+
* - openai/text-embedding-3-small (dimension: 1536)
13+
* - openai/text-embedding-3-large (dimension: 3072)
14+
* - openai/text-embedding-ada-002 (dimension: 1536)
15+
* - cohere/embed-english-v3.0 (dimension: 1024)
16+
* - cohere/embed-multilingual-v3.0 (dimension: 1024)
17+
* - voyage/voyage-3 (dimension: 1024)
18+
* - voyage/voyage-3-lite (dimension: 512)
19+
*/
20+
export class OpenRouterEmbedder extends OpenAICompatibleEmbedder implements IEmbedder {
21+
private static readonly OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
22+
private static readonly DEFAULT_MODEL = "openai/text-embedding-3-small"
23+
private readonly modelId: string
24+
25+
/**
26+
* Creates a new OpenRouter embedder
27+
* @param apiKey The OpenRouter API key for authentication
28+
* @param modelId The model ID to use (defaults to openai/text-embedding-3-small)
29+
* @param baseUrl Optional custom base URL for OpenRouter API (defaults to https://openrouter.ai/api/v1)
30+
*/
31+
constructor(apiKey?: string, modelId?: string, baseUrl?: string) {
32+
if (!apiKey) {
33+
throw new Error(t("embeddings:validation.apiKeyRequired"))
34+
}
35+
36+
// Use the provided base URL or default to OpenRouter's API URL
37+
const openRouterBaseUrl = baseUrl || OpenRouterEmbedder.OPENROUTER_BASE_URL
38+
39+
// Initialize the parent OpenAI Compatible embedder with OpenRouter configuration
40+
super(openRouterBaseUrl, apiKey, modelId || OpenRouterEmbedder.DEFAULT_MODEL, MAX_ITEM_TOKENS)
41+
42+
this.modelId = modelId || getDefaultModelId("openrouter")
43+
}
44+
45+
/**
46+
* Returns information about this embedder
47+
*/
48+
override get embedderInfo(): EmbedderInfo {
49+
return {
50+
name: "openrouter",
51+
}
52+
}
53+
}

src/services/code-index/interfaces/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface CodeIndexConfig {
1515
geminiOptions?: { apiKey: string }
1616
mistralOptions?: { apiKey: string }
1717
vercelAiGatewayOptions?: { apiKey: string }
18+
openRouterOptions?: { apiKey: string; baseUrl?: string }
1819
qdrantUrl?: string
1920
qdrantApiKey?: string
2021
searchMinScore?: number
@@ -37,6 +38,8 @@ export type PreviousConfigSnapshot = {
3738
geminiApiKey?: string
3839
mistralApiKey?: string
3940
vercelAiGatewayApiKey?: string
41+
openRouterApiKey?: string
42+
openRouterBaseUrl?: string
4043
qdrantUrl?: string
4144
qdrantApiKey?: string
4245
}

src/services/code-index/interfaces/embedder.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,14 @@ export interface EmbeddingResponse {
2828
}
2929
}
3030

31-
export type AvailableEmbedders = "openai" | "ollama" | "openai-compatible" | "gemini" | "mistral" | "vercel-ai-gateway"
31+
export type AvailableEmbedders =
32+
| "openai"
33+
| "ollama"
34+
| "openai-compatible"
35+
| "gemini"
36+
| "mistral"
37+
| "vercel-ai-gateway"
38+
| "openrouter"
3239

3340
export interface EmbedderInfo {
3441
name: AvailableEmbedders

src/services/code-index/interfaces/manager.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,14 @@ export interface ICodeIndexManager {
7070
}
7171

7272
export type IndexingState = "Standby" | "Indexing" | "Indexed" | "Error"
73-
export type EmbedderProvider = "openai" | "ollama" | "openai-compatible" | "gemini" | "mistral" | "vercel-ai-gateway"
73+
export type EmbedderProvider =
74+
| "openai"
75+
| "ollama"
76+
| "openai-compatible"
77+
| "gemini"
78+
| "mistral"
79+
| "vercel-ai-gateway"
80+
| "openrouter"
7481

7582
export interface IndexProgressUpdate {
7683
systemStatus: IndexingState

src/services/code-index/service-factory.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { OpenAICompatibleEmbedder } from "./embedders/openai-compatible"
55
import { GeminiEmbedder } from "./embedders/gemini"
66
import { MistralEmbedder } from "./embedders/mistral"
77
import { VercelAiGatewayEmbedder } from "./embedders/vercel-ai-gateway"
8+
import { OpenRouterEmbedder } from "./embedders/openrouter"
89
import { EmbedderProvider, getDefaultModelId, getModelDimension } from "../../shared/embeddingModels"
910
import { QdrantVectorStore } from "./vector-store/qdrant-client"
1011
import { codeParser, DirectoryScanner, FileWatcher } from "./processors"
@@ -79,6 +80,15 @@ export class CodeIndexServiceFactory {
7980
throw new Error(t("embeddings:serviceFactory.vercelAiGatewayConfigMissing"))
8081
}
8182
return new VercelAiGatewayEmbedder(config.vercelAiGatewayOptions.apiKey, config.modelId)
83+
} else if (provider === "openrouter") {
84+
if (!config.openRouterOptions?.apiKey) {
85+
throw new Error(t("embeddings:serviceFactory.openRouterConfigMissing"))
86+
}
87+
return new OpenRouterEmbedder(
88+
config.openRouterOptions.apiKey,
89+
config.modelId,
90+
config.openRouterOptions.baseUrl,
91+
)
8292
}
8393

8494
throw new Error(

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ export interface WebviewMessage {
292292
| "gemini"
293293
| "mistral"
294294
| "vercel-ai-gateway"
295+
| "openrouter"
295296
codebaseIndexEmbedderBaseUrl?: string
296297
codebaseIndexEmbedderModelId: string
297298
codebaseIndexEmbedderModelDimension?: number // Generic dimension for all providers

src/shared/embeddingModels.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22
* Defines profiles for different embedding models, including their dimensions.
33
*/
44

5-
export type EmbedderProvider = "openai" | "ollama" | "openai-compatible" | "gemini" | "mistral" | "vercel-ai-gateway" // Add other providers as needed
5+
export type EmbedderProvider =
6+
| "openai"
7+
| "ollama"
8+
| "openai-compatible"
9+
| "gemini"
10+
| "mistral"
11+
| "vercel-ai-gateway"
12+
| "openrouter" // Add other providers as needed
613

714
export interface EmbeddingModelProfile {
815
dimension: number
@@ -70,6 +77,19 @@ export const EMBEDDING_MODEL_PROFILES: EmbeddingModelProfiles = {
7077
"mistral/codestral-embed": { dimension: 1536, scoreThreshold: 0.4 },
7178
"mistral/mistral-embed": { dimension: 1024, scoreThreshold: 0.4 },
7279
},
80+
openrouter: {
81+
// OpenAI models available via OpenRouter
82+
"openai/text-embedding-3-small": { dimension: 1536, scoreThreshold: 0.4 },
83+
"openai/text-embedding-3-large": { dimension: 3072, scoreThreshold: 0.4 },
84+
"openai/text-embedding-ada-002": { dimension: 1536, scoreThreshold: 0.4 },
85+
// Cohere models
86+
"cohere/embed-english-v3.0": { dimension: 1024, scoreThreshold: 0.4 },
87+
"cohere/embed-multilingual-v3.0": { dimension: 1024, scoreThreshold: 0.4 },
88+
// Voyage models
89+
"voyage/voyage-embeddings-v3": { dimension: 1024, scoreThreshold: 0.4 },
90+
"voyage/voyage-3": { dimension: 1024, scoreThreshold: 0.4 },
91+
"voyage/voyage-3-lite": { dimension: 512, scoreThreshold: 0.4 },
92+
},
7393
}
7494

7595
/**
@@ -163,6 +183,9 @@ export function getDefaultModelId(provider: EmbedderProvider): string {
163183
case "vercel-ai-gateway":
164184
return "openai/text-embedding-3-large"
165185

186+
case "openrouter":
187+
return "openai/text-embedding-3-small"
188+
166189
default:
167190
// Fallback for unknown providers
168191
console.warn(`Unknown provider for default model ID: ${provider}. Falling back to OpenAI default.`)

0 commit comments

Comments
 (0)