Files
OmniRoute/open-sse/config/embeddingRegistry.ts
diegosouzapw a086862c97 docs: add 3.8.7 changelog and release branch guidance
Update the main and localized changelogs with the 3.8.7 release notes, including new API self-service status, analytics retention, RAM optimizations, and token accounting fixes.

Clarify release skill instructions to ensure new release branches are always created from the latest main branch.
2026-05-29 13:57:57 -03:00

396 lines
13 KiB
TypeScript

/**
* Embedding Provider Registry
*
* Defines providers that support the /v1/embeddings endpoint.
* All providers use the OpenAI-compatible format.
*
* API keys are stored in the same provider credentials system,
* keyed by provider ID (e.g. "nebius", "openai").
*/
export interface EmbeddingProvider {
id: string;
baseUrl: string;
authType: string;
authHeader: string;
models: { id: string; name: string; dimensions?: number }[];
}
export interface EmbeddingProviderNodeRow {
id?: string;
prefix: string;
name: string;
baseUrl: string;
apiType?: string;
}
/**
* Build a dynamic EmbeddingProvider from a local provider_node.
* Only used for local providers (localhost) — caller must filter by hostname.
*/
export function buildDynamicEmbeddingProvider(node: EmbeddingProviderNodeRow): EmbeddingProvider {
if (!node.prefix || !node.baseUrl) {
throw new Error(`Invalid provider_node: missing prefix or baseUrl`);
}
if (node.prefix.includes("/") || node.prefix.includes(" ")) {
throw new Error(`Invalid provider_node prefix "${node.prefix}": must not contain / or spaces`);
}
const baseUrl = node.baseUrl.replace(/\/+$/, "");
return {
id: node.prefix,
baseUrl: `${baseUrl}/embeddings`,
authType: "none",
authHeader: "none",
models: [],
};
}
let _EMBEDDING_PROVIDERS: Record<string, EmbeddingProvider> | null = null;
function getOrCreateEmbeddingProviders(): Record<string, EmbeddingProvider> {
if (!_EMBEDDING_PROVIDERS) {
_EMBEDDING_PROVIDERS = {
cohere: {
id: "cohere",
baseUrl: "https://api.cohere.com/v2/embed",
authType: "apikey",
authHeader: "bearer",
models: [
{ id: "embed-v4.0", name: "Embed v4.0" },
{ id: "embed-multilingual-v3.0", name: "Embed Multilingual v3.0" },
{ id: "embed-multilingual-v3.0-images", name: "Embed Multilingual v3.0 Image" },
{ id: "embed-multilingual-light-v3.0", name: "Embed Multilingual Light v3.0" },
{ id: "embed-multilingual-light-v3.0-images", name: "Embed Multilingual Light v3.0 Image" },
],
},
nebius: {
id: "nebius",
baseUrl: "https://api.tokenfactory.nebius.com/v1/embeddings",
authType: "apikey",
authHeader: "bearer",
models: [{ id: "Qwen/Qwen3-Embedding-8B", name: "Qwen3 Embedding 8B", dimensions: 4096 }],
},
openai: {
id: "openai",
baseUrl: "https://api.openai.com/v1/embeddings",
authType: "apikey",
authHeader: "bearer",
models: [
{ id: "text-embedding-3-small", name: "Text Embedding 3 Small", dimensions: 1536 },
{ id: "text-embedding-3-large", name: "Text Embedding 3 Large", dimensions: 3072 },
{ id: "text-embedding-ada-002", name: "Text Embedding Ada 002", dimensions: 1536 },
],
},
upstage: {
id: "upstage",
baseUrl: "https://api.upstage.ai/v1/embeddings",
authType: "apikey",
authHeader: "bearer",
models: [
{ id: "embedding-query", name: "Embedding Query", dimensions: 4096 },
{ id: "embedding-passage", name: "Embedding Passage", dimensions: 4096 },
],
},
mistral: {
id: "mistral",
baseUrl: "https://api.mistral.ai/v1/embeddings",
authType: "apikey",
authHeader: "bearer",
models: [{ id: "mistral-embed", name: "Mistral Embed", dimensions: 1024 }],
},
together: {
id: "together",
baseUrl: "https://api.together.xyz/v1/embeddings",
authType: "apikey",
authHeader: "bearer",
models: [
{ id: "BAAI/bge-large-en-v1.5", name: "BGE Large EN v1.5", dimensions: 1024 },
{ id: "togethercomputer/m2-bert-80M-8k-retrieval", name: "M2 BERT 80M 8K", dimensions: 768 },
],
},
fireworks: {
id: "fireworks",
baseUrl: "https://api.fireworks.ai/inference/v1/embeddings",
authType: "apikey",
authHeader: "bearer",
models: [
{ id: "nomic-ai/nomic-embed-text-v1.5", name: "Nomic Embed Text v1.5", dimensions: 768 },
{
id: "accounts/fireworks/models/qwen3-embedding-8b",
name: "Qwen3 Embedding 8B",
dimensions: 4096,
},
],
},
nvidia: {
id: "nvidia",
baseUrl: "https://integrate.api.nvidia.com/v1/embeddings",
authType: "apikey",
authHeader: "bearer",
models: [{ id: "nvidia/nv-embedqa-e5-v5", name: "NV EmbedQA E5 v5", dimensions: 1024 }],
},
// Issue #2298: Adding DeepInfra to the embedding registry so custom
// embedding models on the DeepInfra provider don't fail with "Unknown
// embedding provider" when the user adds them via the dashboard.
deepinfra: {
id: "deepinfra",
baseUrl: "https://api.deepinfra.com/v1/openai/embeddings",
authType: "apikey",
authHeader: "bearer",
models: [
{ id: "Qwen/Qwen3-Embedding-8B", name: "Qwen3 Embedding 8B", dimensions: 4096 },
{ id: "Qwen/Qwen3-Embedding-4B", name: "Qwen3 Embedding 4B", dimensions: 2560 },
{ id: "Qwen/Qwen3-Embedding-0.6B", name: "Qwen3 Embedding 0.6B", dimensions: 1024 },
{ id: "BAAI/bge-large-en-v1.5", name: "BGE Large EN v1.5", dimensions: 1024 },
{ id: "BAAI/bge-base-en-v1.5", name: "BGE Base EN v1.5", dimensions: 768 },
{ id: "BAAI/bge-m3", name: "BGE-M3", dimensions: 1024 },
{ id: "intfloat/e5-large-v2", name: "E5 Large v2", dimensions: 1024 },
{ id: "thenlper/gte-large", name: "GTE Large", dimensions: 1024 },
],
},
openrouter: {
id: "openrouter",
baseUrl: "https://openrouter.ai/api/v1/embeddings",
authType: "apikey",
authHeader: "bearer",
models: [
{
id: "openai/text-embedding-3-small",
name: "Text Embedding 3 Small (OpenRouter)",
dimensions: 1536,
},
{
id: "openai/text-embedding-3-large",
name: "Text Embedding 3 Large (OpenRouter)",
dimensions: 3072,
},
{
id: "openai/text-embedding-ada-002",
name: "Text Embedding Ada 002 (OpenRouter)",
dimensions: 1536,
},
],
},
gemini: {
id: "gemini",
baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai/embeddings",
authType: "apikey",
authHeader: "bearer",
models: [{ id: "text-embedding-004", name: "Text Embedding 004", dimensions: 768 }],
},
"voyage-ai": {
id: "voyage-ai",
baseUrl: "https://api.voyageai.com/v1/embeddings",
authType: "apikey",
authHeader: "bearer",
models: [
{ id: "voyage-4-large", name: "Voyage 4 Large", dimensions: 1024 },
{ id: "voyage-4", name: "Voyage 4", dimensions: 1024 },
{ id: "voyage-4-lite", name: "Voyage 4 Lite", dimensions: 1024 },
{ id: "voyage-3-large", name: "Voyage 3 Large", dimensions: 1024 },
{ id: "voyage-multilingual-3.5", name: "Voyage Multilingual 3.5", dimensions: 1024 },
{ id: "voyage-code-3", name: "Voyage Code 3", dimensions: 1024 },
{ id: "voyage-code-2", name: "Voyage Code 2", dimensions: 1536 },
{ id: "voyage-finance-2", name: "Voyage Finance 2", dimensions: 1024 },
{ id: "voyage-law-2", name: "Voyage Law 2", dimensions: 1024 },
],
},
github: {
id: "github",
baseUrl: "https://models.inference.ai.azure.com/embeddings",
authType: "apikey",
authHeader: "bearer",
models: [
{ id: "text-embedding-3-small", name: "Text Embedding 3 Small (GitHub)", dimensions: 1536 },
{ id: "text-embedding-3-large", name: "Text Embedding 3 Large (GitHub)", dimensions: 3072 },
],
},
"jina-ai": {
id: "jina-ai",
baseUrl: "https://api.jina.ai/v1/embeddings",
authType: "apikey",
authHeader: "bearer",
models: [
{
id: "jina-embeddings-v5-text-small",
name: "Jina Embeddings v5 Text Small",
dimensions: 1024,
},
{ id: "jina-embeddings-v5-text-nano", name: "Jina Embeddings v5 Text Nano", dimensions: 768 },
{ id: "jina-code-embeddings-1.5b", name: "Jina Code Embeddings 1.5B", dimensions: 1536 },
{ id: "jina-code-embeddings-0.5b", name: "Jina Code Embeddings 0.5B", dimensions: 896 },
{ id: "jina-embeddings-v4", name: "Jina Embeddings v4", dimensions: 2048 },
{ id: "jina-clip-v2", name: "Jina CLIP v2", dimensions: 1024 },
{ id: "jina-colbert-v2", name: "Jina ColBERT v2", dimensions: 128 },
],
},
};
}
return _EMBEDDING_PROVIDERS;
}
export const EMBEDDING_PROVIDERS: Record<string, EmbeddingProvider> = new Proxy({} as Record<string, EmbeddingProvider>, {
get(target, key: string) {
if (key in target) {
return target[key];
}
return getOrCreateEmbeddingProviders()[key];
},
set(target, key: string, value) {
target[key] = value;
getOrCreateEmbeddingProviders()[key] = value;
return true;
},
deleteProperty(target, key: string) {
delete target[key];
delete getOrCreateEmbeddingProviders()[key];
return true;
},
ownKeys(target) {
const targetKeys = Reflect.ownKeys(target);
const registryKeys = Reflect.ownKeys(getOrCreateEmbeddingProviders());
return Array.from(new Set([...targetKeys, ...registryKeys]));
},
has(target, key) {
return key in target || key in getOrCreateEmbeddingProviders();
},
getOwnPropertyDescriptor(target, key) {
if (key in target) {
return Reflect.getOwnPropertyDescriptor(target, key);
}
if (key in getOrCreateEmbeddingProviders()) {
return { configurable: true, enumerable: true, value: getOrCreateEmbeddingProviders()[key as string] };
}
return undefined;
},
});
export function getEmbeddingProviders(): Record<string, EmbeddingProvider> {
return EMBEDDING_PROVIDERS;
}
const EMBEDDING_PROVIDER_ALIASES: Record<string, string> = {
jina: "jina-ai",
voyage: "voyage-ai",
};
function resolveEmbeddingProviderId(providerId: string): string {
return EMBEDDING_PROVIDER_ALIASES[providerId] || providerId;
}
function normalizeProviderScopedModelId(providerId: string, modelId: string): string {
const resolvedProvider = resolveEmbeddingProviderId(providerId);
const provider = EMBEDDING_PROVIDERS[resolvedProvider];
if (provider?.models.some((model) => model.id === modelId)) return modelId;
const providerScopedModelId = `${resolvedProvider}/${modelId}`;
if (provider?.models.some((model) => model.id === providerScopedModelId)) {
return providerScopedModelId;
}
return modelId.startsWith(`${providerId}/`) ? modelId.slice(providerId.length + 1) : modelId;
}
function toProviderScopedModelId(providerId: string, modelId: string): string {
return modelId.startsWith(`${providerId}/`) ? modelId : `${providerId}/${modelId}`;
}
/**
* Get embedding provider config by ID
*/
export function getEmbeddingProvider(providerId: string): EmbeddingProvider | null {
return EMBEDDING_PROVIDERS[resolveEmbeddingProviderId(providerId)] || null;
}
/**
* Parse embedding model string (format: "provider/model" or just "model")
* Returns { provider, model }
*/
export function parseEmbeddingModel(
modelStr: string | null,
dynamicProviders?: EmbeddingProvider[]
): { provider: string | null; model: string | null } {
if (!modelStr) return { provider: null, model: null };
// Check for "provider/model" format
const slashIdx = modelStr.indexOf("/");
if (slashIdx > 0) {
const rawProvider = modelStr.slice(0, slashIdx);
const resolvedProvider = resolveEmbeddingProviderId(rawProvider);
if (EMBEDDING_PROVIDERS[resolvedProvider]) {
return {
provider: resolvedProvider,
model: normalizeProviderScopedModelId(resolvedProvider, modelStr.slice(slashIdx + 1)),
};
}
// Phase 1: Try each hardcoded provider prefix
for (const [providerId] of Object.entries(EMBEDDING_PROVIDERS)) {
if (modelStr.startsWith(providerId + "/")) {
return {
provider: providerId,
model: normalizeProviderScopedModelId(providerId, modelStr.slice(providerId.length + 1)),
};
}
}
// Phase 2: Try dynamic provider_nodes prefix
if (dynamicProviders) {
for (const dp of dynamicProviders) {
if (modelStr.startsWith(dp.id + "/")) {
return { provider: dp.id, model: modelStr.slice(dp.id.length + 1) };
}
}
}
// Phase 3: Fallback — first segment is provider
const provider = modelStr.slice(0, slashIdx);
const model = modelStr.slice(slashIdx + 1);
return { provider, model };
}
// No provider prefix — search hardcoded providers for the model
for (const [providerId, config] of Object.entries(EMBEDDING_PROVIDERS)) {
if (config.models.some((m) => m.id === modelStr)) {
return { provider: providerId, model: modelStr };
}
}
return { provider: null, model: modelStr };
}
/**
* Get all embedding models as a flat list
*/
export function getAllEmbeddingModels() {
const models: Array<{
id: string;
name: string;
provider: string;
dimensions: number | undefined;
}> = [];
for (const [providerId, config] of Object.entries(EMBEDDING_PROVIDERS)) {
for (const model of config.models) {
models.push({
id: toProviderScopedModelId(providerId, model.id),
name: model.name,
provider: providerId,
dimensions: model.dimensions,
});
}
}
return models;
}