Files
OmniRoute/tests/unit/rerank-generic-provider-fallback.test.ts
Rouzbeh† 451dd73870 fix(memory): list and serve embedding/rerank models from every configured provider (#11390)
On dashboard/memory?tab=engine the Embedding Model quick-select (and the rerank selector) built their lists from a keyword heuristic over the CHAT catalog (AI_MODELS) plus OpenRouter live discovery. Providers whose embedding models are not in that catalog never appeared — mistral, gemini, nvidia nim, groq, vercel-ai-gateway and others that serve embeddings on a standard OpenAI-compatible /embeddings endpoint — and typing such a model by hand failed at runtime with "Unknown embedding provider".

The fix is one generic mechanism rather than a list of per-provider patches: deriveEmbeddingProviderForChatProvider() turns any chat-registry entry with a /chat/completions base into an OpenAI-compatible /embeddings config, with curated EMBEDDING_PROVIDERS entries always winning; the embeddings service resolves a derived config for unknown-but-configured providers instead of rejecting them; deriveRerankProviderForChatProvider() does the same for Cohere-compatible /rerank; and both memory selectors fall back to a free-text provider/model input when no static catalog exists. No provider is special-cased by name, so adding one to the chat registry now makes it embedding- and rerank-capable here automatically.

Verified on the current release tip: merged clean, typecheck:core clean, check:cycles OK across 417 files, and 35/35 across the PR's five new suites (qdrant-quick-select-catalog, memory-provider-listings, rerank-provider-listings, embedding-generic-provider-fallback, rerank-generic-provider-fallback) plus the updated hard-session-lease-bypass-inventory and embeddings-handler.

Note: the base-red disclaimer in the description referenced #9985 against release/v3.8.50 — that window is closed and the current tip carries no open base-red, so nothing was inherited here.

Thanks @rqzbeh — deriving the capability instead of enumerating providers is the version of this that stays correct as the registry grows.
2026-09-02 00:27:29 -03:00

70 lines
3.0 KiB
TypeScript

/**
* Issue: rerank model strings outside the curated RERANK_PROVIDERS registry were
* rejected with "No rerank provider found" even when the provider was configured
* in OmniRoute with a working Cohere-compatible /rerank endpoint (e.g. groq,
* siliconflow-style hosts). The memory Rerank selector fed by the curated list
* had the same blind spot.
*
* These tests pin: parseRerankModel keeps returning null provider for unknown
* prefixes (registry semantics unchanged), and the new generic fallback builder
* derives a Cohere-compatible config for any configured OpenAI-compatible chat
* provider without shadowing curated entries.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { REGISTRY } from "@omniroute/open-sse/config/providerRegistry.ts";
import {
getRerankProvider,
parseRerankModel,
} from "../../open-sse/config/rerankRegistry.ts";
import { deriveRerankProviderForChatProvider } from "../../open-sse/config/rerankRegistry.ts";
describe("rerank registry: unknown providers stay rejected at parse level", () => {
it("parseRerankModel returns provider null for an unregistered prefix", () => {
const parsed = parseRerankModel("groq/some-reranker");
assert.equal(parsed.provider, null);
// Registry semantics: when no curated provider matches, model keeps its
// full original string (provider prefix included).
assert.equal(parsed.model, "groq/some-reranker");
assert.equal(getRerankProvider("groq"), null);
});
});
describe("deriveRerankProviderForChatProvider (generic Cohere-compatible fallback)", () => {
it("derives a /rerank endpoint from a chat-completions base URL", () => {
const derived = deriveRerankProviderForChatProvider("groq", {
id: "groq",
baseUrl: "https://api.groq.com/openai/v1/chat/completions",
});
assert.ok(derived, "groq should derive a rerank provider");
assert.equal(derived.baseUrl, "https://api.groq.com/openai/v1/rerank");
assert.deepEqual(derived.models, []);
});
it("returns null for dynamic-URL providers and missing entries", () => {
assert.equal(
deriveRerankProviderForChatProvider("account-scoped", {
id: "account-scoped",
baseUrl: "https://api.example.com/client/v4/accounts",
}),
null
);
assert.equal(deriveRerankProviderForChatProvider("ghost", undefined), null);
});
it("does not shadow curated rerank registries", () => {
for (const id of ["cohere", "together", "siliconflow", "voyage-ai", "jina-ai"]) {
const entry = REGISTRY[id] as { baseUrl?: string } | undefined;
if (!entry) continue;
const derived = deriveRerankProviderForChatProvider(id, entry);
if (derived) {
assert.ok(getRerankProvider(id), `${id} remains curated; derivation is fallback-only`);
}
}
// cohere IS curated — helper still derives mechanically, but callers must
// check the curated registry first. Pin that ordering here:
const curated = getRerankProvider("cohere");
assert.ok(curated?.models.length, "curated cohere entry has models");
});
});