mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 13:52:09 +03:00
Integrated into release/v3.8.41 — dead-code removal (typecheck:core EXIT 0, 102 affected tests green, fabricated-docs clean). Thanks @JxnLexn.
282 lines
8.4 KiB
TypeScript
282 lines
8.4 KiB
TypeScript
import {
|
|
EMBEDDING_PROVIDERS,
|
|
buildDynamicEmbeddingProvider,
|
|
type EmbeddingProviderNodeRow,
|
|
} from "@omniroute/open-sse/config/embeddingRegistry.ts";
|
|
import { getProviderCredentials } from "@/sse/services/auth";
|
|
import { getProviderNodes } from "@/lib/localDb";
|
|
import type { MemorySettingsExtended } from "@/shared/schemas/memory";
|
|
import type {
|
|
EmbeddingResolution,
|
|
EmbeddingResult,
|
|
EmbeddingError,
|
|
EmbeddingProviderListing,
|
|
} from "./types";
|
|
import { embedRemote } from "./remote";
|
|
import { embedStatic } from "./staticPotion";
|
|
import { embedTransformers } from "./transformersLocal";
|
|
import { buildCacheKey, get as cacheGet, set as cacheSet } from "./cache";
|
|
|
|
const STATIC_MODEL = process.env.MEMORY_STATIC_MODEL || "minishlab/potion-base-8M";
|
|
const TRANSFORMERS_MODEL = process.env.MEMORY_TRANSFORMERS_MODEL || "Xenova/all-MiniLM-L6-v2";
|
|
|
|
/** Build an EmbeddingResolution for "no source available" cases. */
|
|
function noSource(reason: string): EmbeddingResolution {
|
|
return {
|
|
source: null,
|
|
model: null,
|
|
dimensions: null,
|
|
signature: "null:null:null",
|
|
reason,
|
|
};
|
|
}
|
|
|
|
/** Build a signature string. */
|
|
function makeSignature(
|
|
source: "remote" | "static" | "transformers" | null,
|
|
model: string | null,
|
|
dim: number | null
|
|
): string {
|
|
return `${source ?? "null"}:${model ?? "null"}:${dim ?? "null"}`;
|
|
}
|
|
|
|
/**
|
|
* Resolve which embedding source is active for the given settings (D4).
|
|
* Pure: no heavy I/O. Provider key check done via synchronous registry lookup.
|
|
*/
|
|
export function resolveEmbeddingSource(settings: MemorySettingsExtended): EmbeddingResolution {
|
|
const source = settings.embeddingSource ?? "auto";
|
|
|
|
if (source === "remote") {
|
|
// Explicit remote — check if the configured model has a key
|
|
const model = settings.embeddingProviderModel ?? null;
|
|
if (!model) {
|
|
return {
|
|
source: null,
|
|
model: null,
|
|
dimensions: null,
|
|
signature: makeSignature(null, null, null),
|
|
reason: "no_key: embeddingProviderModel não configurado",
|
|
};
|
|
}
|
|
// We can't do async here, so we report it as potentially available
|
|
// and the caller will attempt embed + get no_key error on failure.
|
|
// For resolution purposes, mark as remote (will fail at embed time if no key).
|
|
return {
|
|
source: "remote",
|
|
model,
|
|
dimensions: null,
|
|
signature: makeSignature("remote", model, null),
|
|
reason: `provider remoto configurado: ${model}`,
|
|
};
|
|
}
|
|
|
|
if (source === "static") {
|
|
if (settings.staticEnabled !== true) {
|
|
return {
|
|
source: null,
|
|
model: null,
|
|
dimensions: null,
|
|
signature: makeSignature(null, null, null),
|
|
reason: "static desabilitado nas configurações",
|
|
};
|
|
}
|
|
return {
|
|
source: "static",
|
|
model: STATIC_MODEL,
|
|
dimensions: 256,
|
|
signature: makeSignature("static", STATIC_MODEL, 256),
|
|
reason: "static (potion-base-8M) selecionado explicitamente",
|
|
};
|
|
}
|
|
|
|
if (source === "transformers") {
|
|
if (settings.transformersEnabled !== true) {
|
|
return {
|
|
source: null,
|
|
model: null,
|
|
dimensions: null,
|
|
signature: makeSignature(null, null, null),
|
|
reason: "transformers desabilitado nas configurações",
|
|
};
|
|
}
|
|
return {
|
|
source: "transformers",
|
|
model: TRANSFORMERS_MODEL,
|
|
dimensions: 384,
|
|
signature: makeSignature("transformers", TRANSFORMERS_MODEL, 384),
|
|
reason: "transformers.js (MiniLM-L6-v2) selecionado explicitamente",
|
|
};
|
|
}
|
|
|
|
// auto: (1) remote if model configured and provider has key in registry
|
|
// (2) static if staticEnabled
|
|
// (3) transformers if transformersEnabled
|
|
// (4) null
|
|
if (source === "auto") {
|
|
// Try remote first — check if embeddingProviderModel is set
|
|
const providerModel = settings.embeddingProviderModel ?? null;
|
|
if (providerModel) {
|
|
const slashIdx = providerModel.indexOf("/");
|
|
const providerId = slashIdx > 0 ? providerModel.slice(0, slashIdx) : null;
|
|
if (providerId && EMBEDDING_PROVIDERS[providerId]) {
|
|
// We defer the actual hasKey check to listEmbeddingProviders (async).
|
|
// For resolveEmbeddingSource (sync), we report "possibly remote" when model is set.
|
|
// If no key, embed will return EmbeddingError{reason:"no_key"}.
|
|
return {
|
|
source: "remote",
|
|
model: providerModel,
|
|
dimensions: null,
|
|
signature: makeSignature("remote", providerModel, null),
|
|
reason: `auto: provider ${providerId} configurado`,
|
|
};
|
|
}
|
|
}
|
|
|
|
if (settings.staticEnabled === true) {
|
|
return {
|
|
source: "static",
|
|
model: STATIC_MODEL,
|
|
dimensions: 256,
|
|
signature: makeSignature("static", STATIC_MODEL, 256),
|
|
reason: "auto: potion-base-8M (static) disponível",
|
|
};
|
|
}
|
|
|
|
if (settings.transformersEnabled === true) {
|
|
return {
|
|
source: "transformers",
|
|
model: TRANSFORMERS_MODEL,
|
|
dimensions: 384,
|
|
signature: makeSignature("transformers", TRANSFORMERS_MODEL, 384),
|
|
reason: "auto: transformers.js (MiniLM-L6-v2) disponível",
|
|
};
|
|
}
|
|
|
|
return noSource("auto: nenhuma fonte de embedding disponível");
|
|
}
|
|
|
|
return noSource("fonte de embedding desconhecida");
|
|
}
|
|
|
|
/**
|
|
* Generate an embedding for the given text using the active source.
|
|
* Caches results in memory (D6).
|
|
*/
|
|
export async function embed(
|
|
text: string,
|
|
settings: MemorySettingsExtended
|
|
): Promise<EmbeddingResult | EmbeddingError> {
|
|
const resolution = resolveEmbeddingSource(settings);
|
|
|
|
if (!resolution.source) {
|
|
return {
|
|
source: "remote",
|
|
model: null,
|
|
reason: "unknown",
|
|
message: resolution.reason,
|
|
};
|
|
}
|
|
|
|
const cacheKey = buildCacheKey(resolution.source, resolution.model, resolution.dimensions, text);
|
|
|
|
const cached = cacheGet(cacheKey);
|
|
if (cached) {
|
|
return {
|
|
vector: cached,
|
|
source: resolution.source,
|
|
model: resolution.model ?? "",
|
|
dimensions: cached.length,
|
|
latencyMs: 0,
|
|
cached: true,
|
|
};
|
|
}
|
|
|
|
let result: EmbeddingResult | EmbeddingError;
|
|
|
|
if (resolution.source === "remote") {
|
|
result = await embedRemote(text, resolution.model ?? "");
|
|
} else if (resolution.source === "static") {
|
|
result = await embedStatic(text);
|
|
} else {
|
|
result = await embedTransformers(text);
|
|
}
|
|
|
|
if ("vector" in result) {
|
|
cacheSet(cacheKey, result.vector);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* List providers that have embedding models, marking which ones have a configured API key.
|
|
* Aggregates from EMBEDDING_PROVIDERS + local provider_nodes.
|
|
*/
|
|
export async function listEmbeddingProviders(): Promise<EmbeddingProviderListing[]> {
|
|
// Get dynamic local providers
|
|
let dynamicProviders: ReturnType<typeof buildDynamicEmbeddingProvider>[] = [];
|
|
try {
|
|
const nodes = (await getProviderNodes()) as unknown as EmbeddingProviderNodeRow[];
|
|
dynamicProviders = (Array.isArray(nodes) ? nodes : [])
|
|
.filter((n) => {
|
|
const validTypes = ["chat", "responses", "embeddings"];
|
|
return validTypes.includes(n.apiType || "");
|
|
})
|
|
.map((n) => {
|
|
try {
|
|
return buildDynamicEmbeddingProvider(n);
|
|
} catch {
|
|
return null;
|
|
}
|
|
})
|
|
.filter((p): p is NonNullable<typeof p> => p !== null);
|
|
} catch {
|
|
// Ignore failures — just return static providers
|
|
}
|
|
|
|
const result: EmbeddingProviderListing[] = [];
|
|
|
|
// Process hardcoded EMBEDDING_PROVIDERS
|
|
for (const [providerId, config] of Object.entries(EMBEDDING_PROVIDERS)) {
|
|
let hasKey = false;
|
|
try {
|
|
const creds = await getProviderCredentials(providerId);
|
|
hasKey = !!(
|
|
creds &&
|
|
!("allRateLimited" in creds && creds.allRateLimited) &&
|
|
(("apiKey" in creds ? !!creds.apiKey : false) ||
|
|
("accessToken" in creds ? !!creds.accessToken : false))
|
|
);
|
|
} catch {
|
|
hasKey = false;
|
|
}
|
|
|
|
result.push({
|
|
provider: providerId,
|
|
hasKey,
|
|
models: config.models.map((m) => ({
|
|
id: `${providerId}/${m.id}`,
|
|
name: m.name,
|
|
dimensions: m.dimensions ?? null,
|
|
})),
|
|
});
|
|
}
|
|
|
|
// Process dynamic providers (local nodes)
|
|
for (const dp of dynamicProviders) {
|
|
// Dynamic local providers typically have authType="none"
|
|
result.push({
|
|
provider: dp.id,
|
|
hasKey: true, // local providers don't need keys
|
|
models: dp.models.map((m) => ({
|
|
id: `${dp.id}/${m.id}`,
|
|
name: m.name,
|
|
dimensions: m.dimensions ?? null,
|
|
})),
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}
|