Skip to content

Commit 6cb1f2c

Browse files
committed
feat: add OpenRouter as first-class embedding provider
Add dedicated OpenRouter embedding provider that routes through OpenRouter's OpenAI-compatible API. This provides a cleaner UX than using OPENAI_BASE_URL workaround, with proper model naming (e.g., openai/text-embedding-3-small) and configuration. New env vars: EMBEDDING_PROVIDER=OpenRouter, OPENROUTER_API_KEY
1 parent 66d7616 commit 6cb1f2c

4 files changed

Lines changed: 184 additions & 6 deletions

File tree

packages/core/src/embedding/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ export * from './base-embedding';
55
export * from './openai-embedding';
66
export * from './voyageai-embedding';
77
export * from './ollama-embedding';
8-
export * from './gemini-embedding';
8+
export * from './gemini-embedding';
9+
export * from './openrouter-embedding';
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import OpenAI from 'openai';
2+
import { Embedding, EmbeddingVector } from './base-embedding';
3+
4+
export interface OpenRouterEmbeddingConfig {
5+
model: string;
6+
apiKey: string;
7+
baseURL?: string;
8+
}
9+
10+
const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
11+
12+
export class OpenRouterEmbedding extends Embedding {
13+
private client: OpenAI;
14+
private config: OpenRouterEmbeddingConfig;
15+
private dimension: number = 1536;
16+
protected maxTokens: number = 8192;
17+
18+
constructor(config: OpenRouterEmbeddingConfig) {
19+
super();
20+
this.config = config;
21+
this.client = new OpenAI({
22+
apiKey: config.apiKey,
23+
baseURL: config.baseURL || OPENROUTER_BASE_URL,
24+
});
25+
26+
const model = config.model || 'openai/text-embedding-3-small';
27+
const knownModels = OpenRouterEmbedding.getSupportedModels();
28+
if (knownModels[model]) {
29+
this.dimension = knownModels[model].dimension;
30+
this.maxTokens = knownModels[model].maxTokens || 8192;
31+
}
32+
}
33+
34+
async detectDimension(testText: string = "test"): Promise<number> {
35+
const model = this.config.model || 'openai/text-embedding-3-small';
36+
const knownModels = OpenRouterEmbedding.getSupportedModels();
37+
38+
if (knownModels[model]) {
39+
return knownModels[model].dimension;
40+
}
41+
42+
try {
43+
const processedText = this.preprocessText(testText);
44+
const response = await this.client.embeddings.create({
45+
model: model,
46+
input: processedText,
47+
encoding_format: 'float',
48+
});
49+
return response.data[0].embedding.length;
50+
} catch (error) {
51+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
52+
throw new Error(`Failed to detect dimension for model ${model}: ${errorMessage}`);
53+
}
54+
}
55+
56+
async embed(text: string): Promise<EmbeddingVector> {
57+
const processedText = this.preprocessText(text);
58+
const model = this.config.model || 'openai/text-embedding-3-small';
59+
60+
try {
61+
const response = await this.client.embeddings.create({
62+
model: model,
63+
input: processedText,
64+
encoding_format: 'float',
65+
});
66+
67+
this.dimension = response.data[0].embedding.length;
68+
69+
return {
70+
vector: response.data[0].embedding,
71+
dimension: this.dimension
72+
};
73+
} catch (error) {
74+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
75+
throw new Error(`Failed to generate OpenRouter embedding: ${errorMessage}`);
76+
}
77+
}
78+
79+
async embedBatch(texts: string[]): Promise<EmbeddingVector[]> {
80+
const processedTexts = this.preprocessTexts(texts);
81+
const model = this.config.model || 'openai/text-embedding-3-small';
82+
83+
try {
84+
const response = await this.client.embeddings.create({
85+
model: model,
86+
input: processedTexts,
87+
encoding_format: 'float',
88+
});
89+
90+
this.dimension = response.data[0].embedding.length;
91+
92+
return response.data.map((item) => ({
93+
vector: item.embedding,
94+
dimension: this.dimension
95+
}));
96+
} catch (error) {
97+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
98+
throw new Error(`Failed to generate OpenRouter batch embeddings: ${errorMessage}`);
99+
}
100+
}
101+
102+
getDimension(): number {
103+
const model = this.config.model || 'openai/text-embedding-3-small';
104+
const knownModels = OpenRouterEmbedding.getSupportedModels();
105+
106+
if (knownModels[model]) {
107+
return knownModels[model].dimension;
108+
}
109+
110+
return this.dimension;
111+
}
112+
113+
getProvider(): string {
114+
return 'OpenRouter';
115+
}
116+
117+
async setModel(model: string): Promise<void> {
118+
this.config.model = model;
119+
const knownModels = OpenRouterEmbedding.getSupportedModels();
120+
if (knownModels[model]) {
121+
this.dimension = knownModels[model].dimension;
122+
this.maxTokens = knownModels[model].maxTokens || 8192;
123+
} else {
124+
this.dimension = await this.detectDimension();
125+
}
126+
}
127+
128+
getClient(): OpenAI {
129+
return this.client;
130+
}
131+
132+
static getSupportedModels(): Record<string, { dimension: number; maxTokens?: number; description: string }> {
133+
return {
134+
'openai/text-embedding-3-small': {
135+
dimension: 1536,
136+
maxTokens: 8192,
137+
description: 'OpenAI text-embedding-3-small via OpenRouter'
138+
},
139+
'openai/text-embedding-3-large': {
140+
dimension: 3072,
141+
maxTokens: 8192,
142+
description: 'OpenAI text-embedding-3-large via OpenRouter'
143+
},
144+
'openai/text-embedding-ada-002': {
145+
dimension: 1536,
146+
maxTokens: 8192,
147+
description: 'OpenAI text-embedding-ada-002 via OpenRouter'
148+
},
149+
};
150+
}
151+
}

packages/mcp/src/config.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@ export interface ContextMcpConfig {
44
name: string;
55
version: string;
66
// Embedding provider configuration
7-
embeddingProvider: 'OpenAI' | 'VoyageAI' | 'Gemini' | 'Ollama';
7+
embeddingProvider: 'OpenAI' | 'VoyageAI' | 'Gemini' | 'Ollama' | 'OpenRouter';
88
embeddingModel: string;
99
// Provider-specific API keys
1010
openaiApiKey?: string;
1111
openaiBaseUrl?: string;
1212
voyageaiApiKey?: string;
1313
geminiApiKey?: string;
1414
geminiBaseUrl?: string;
15+
// OpenRouter configuration
16+
openrouterApiKey?: string;
1517
// Ollama configuration
1618
ollamaModel?: string;
1719
ollamaHost?: string;
@@ -76,6 +78,8 @@ export function getDefaultModelForProvider(provider: string): string {
7678
return 'voyage-code-3';
7779
case 'Gemini':
7880
return 'gemini-embedding-001';
81+
case 'OpenRouter':
82+
return 'openai/text-embedding-3-small';
7983
case 'Ollama':
8084
return 'nomic-embed-text';
8185
default:
@@ -94,6 +98,7 @@ export function getEmbeddingModelForProvider(provider: string): string {
9498
case 'OpenAI':
9599
case 'VoyageAI':
96100
case 'Gemini':
101+
case 'OpenRouter':
97102
default:
98103
// For all other providers, use EMBEDDING_MODEL or default
99104
const selectedModel = envManager.get('EMBEDDING_MODEL') || getDefaultModelForProvider(provider);
@@ -117,14 +122,16 @@ export function createMcpConfig(): ContextMcpConfig {
117122
name: envManager.get('MCP_SERVER_NAME') || "Context MCP Server",
118123
version: envManager.get('MCP_SERVER_VERSION') || "1.0.0",
119124
// Embedding provider configuration
120-
embeddingProvider: (envManager.get('EMBEDDING_PROVIDER') as 'OpenAI' | 'VoyageAI' | 'Gemini' | 'Ollama') || 'OpenAI',
125+
embeddingProvider: (envManager.get('EMBEDDING_PROVIDER') as 'OpenAI' | 'VoyageAI' | 'Gemini' | 'Ollama' | 'OpenRouter') || 'OpenAI',
121126
embeddingModel: getEmbeddingModelForProvider(envManager.get('EMBEDDING_PROVIDER') || 'OpenAI'),
122127
// Provider-specific API keys
123128
openaiApiKey: envManager.get('OPENAI_API_KEY'),
124129
openaiBaseUrl: envManager.get('OPENAI_BASE_URL'),
125130
voyageaiApiKey: envManager.get('VOYAGEAI_API_KEY'),
126131
geminiApiKey: envManager.get('GEMINI_API_KEY'),
127132
geminiBaseUrl: envManager.get('GEMINI_BASE_URL'),
133+
// OpenRouter configuration
134+
openrouterApiKey: envManager.get('OPENROUTER_API_KEY'),
128135
// Ollama configuration
129136
ollamaModel: envManager.get('OLLAMA_MODEL'),
130137
ollamaHost: envManager.get('OLLAMA_HOST'),
@@ -162,6 +169,9 @@ export function logConfigurationSummary(config: ContextMcpConfig): void {
162169
console.log(`[MCP] Gemini Base URL: ${config.geminiBaseUrl}`);
163170
}
164171
break;
172+
case 'OpenRouter':
173+
console.log(`[MCP] OpenRouter API Key: ${config.openrouterApiKey ? '✅ Configured' : '❌ Missing'}`);
174+
break;
165175
case 'Ollama':
166176
console.log(`[MCP] Ollama Host: ${config.ollamaHost || 'http://127.0.0.1:11434'}`);
167177
console.log(`[MCP] Ollama Model: ${config.embeddingModel}`);

packages/mcp/src/embedding.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { OpenAIEmbedding, VoyageAIEmbedding, GeminiEmbedding, OllamaEmbedding } from "@zilliz/claude-context-core";
1+
import { OpenAIEmbedding, VoyageAIEmbedding, GeminiEmbedding, OllamaEmbedding, OpenRouterEmbedding } from "@zilliz/claude-context-core";
22
import { ContextMcpConfig } from "./config.js";
33

44
// Helper function to create embedding instance based on provider
5-
export function createEmbeddingInstance(config: ContextMcpConfig): OpenAIEmbedding | VoyageAIEmbedding | GeminiEmbedding | OllamaEmbedding {
5+
export function createEmbeddingInstance(config: ContextMcpConfig): OpenAIEmbedding | VoyageAIEmbedding | GeminiEmbedding | OllamaEmbedding | OpenRouterEmbedding {
66
console.log(`[EMBEDDING] Creating ${config.embeddingProvider} embedding instance...`);
77

88
switch (config.embeddingProvider) {
@@ -47,6 +47,19 @@ export function createEmbeddingInstance(config: ContextMcpConfig): OpenAIEmbeddi
4747
console.log(`[EMBEDDING] ✅ Gemini embedding instance created successfully`);
4848
return geminiEmbedding;
4949

50+
case 'OpenRouter':
51+
if (!config.openrouterApiKey) {
52+
console.error(`[EMBEDDING] ❌ OpenRouter API key is required but not provided`);
53+
throw new Error('OPENROUTER_API_KEY is required for OpenRouter embedding provider');
54+
}
55+
console.log(`[EMBEDDING] 🔧 Configuring OpenRouter with model: ${config.embeddingModel}`);
56+
const openrouterEmbedding = new OpenRouterEmbedding({
57+
apiKey: config.openrouterApiKey,
58+
model: config.embeddingModel,
59+
});
60+
console.log(`[EMBEDDING] ✅ OpenRouter embedding instance created successfully`);
61+
return openrouterEmbedding;
62+
5063
case 'Ollama':
5164
const ollamaHost = config.ollamaHost || 'http://127.0.0.1:11434';
5265
console.log(`[EMBEDDING] 🔧 Configuring Ollama with model: ${config.embeddingModel}, host: ${ollamaHost}`);
@@ -63,7 +76,7 @@ export function createEmbeddingInstance(config: ContextMcpConfig): OpenAIEmbeddi
6376
}
6477
}
6578

66-
export function logEmbeddingProviderInfo(config: ContextMcpConfig, embedding: OpenAIEmbedding | VoyageAIEmbedding | GeminiEmbedding | OllamaEmbedding): void {
79+
export function logEmbeddingProviderInfo(config: ContextMcpConfig, embedding: OpenAIEmbedding | VoyageAIEmbedding | GeminiEmbedding | OllamaEmbedding | OpenRouterEmbedding): void {
6780
console.log(`[EMBEDDING] ✅ Successfully initialized ${config.embeddingProvider} embedding provider`);
6881
console.log(`[EMBEDDING] Provider details - Model: ${config.embeddingModel}, Dimension: ${embedding.getDimension()}`);
6982

@@ -78,6 +91,9 @@ export function logEmbeddingProviderInfo(config: ContextMcpConfig, embedding: Op
7891
case 'Gemini':
7992
console.log(`[EMBEDDING] Gemini configuration - API Key: ${config.geminiApiKey ? '✅ Provided' : '❌ Missing'}, Base URL: ${config.geminiBaseUrl || 'Default'}`);
8093
break;
94+
case 'OpenRouter':
95+
console.log(`[EMBEDDING] OpenRouter configuration - API Key: ${config.openrouterApiKey ? '✅ Provided' : '❌ Missing'}`);
96+
break;
8197
case 'Ollama':
8298
console.log(`[EMBEDDING] Ollama configuration - Host: ${config.ollamaHost || 'http://127.0.0.1:11434'}, Model: ${config.embeddingModel}`);
8399
break;

0 commit comments

Comments
 (0)