mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 04:42:10 +03:00
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.
396 lines
13 KiB
TypeScript
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;
|
|
}
|