From 527da6565d67366474a66c084c922d73017a166f Mon Sep 17 00:00:00 2001 From: Rafa Martins <146174365+rafacpti23@users.noreply.github.com> Date: Sun, 23 Aug 2026 14:42:09 -0300 Subject: [PATCH] feat: list embeddings from configured providers (#11249) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validated: qdrant-routes integration suite 19/19 on the rebased branch, typecheck:core clean. I retargeted the PR from main to release/v3.8.50 and rebased your single commit onto the release tip (authorship untouched) — no content changes. Embedding models now list from configured/credentialed providers via the embedding registry, with vector dimensions in labels and the unconfigured OpenAI fallback removed. Thank you @rafacpti23! --- .../settings/qdrant/embedding-models/route.ts | 74 +++++++++---------- tests/integration/qdrant-routes.test.ts | 27 ++++++- 2 files changed, 59 insertions(+), 42 deletions(-) diff --git a/src/app/api/settings/qdrant/embedding-models/route.ts b/src/app/api/settings/qdrant/embedding-models/route.ts index 9683e270f4..a0cd0b3441 100644 --- a/src/app/api/settings/qdrant/embedding-models/route.ts +++ b/src/app/api/settings/qdrant/embedding-models/route.ts @@ -1,20 +1,17 @@ import { NextRequest, NextResponse } from "next/server"; import { isAuthenticated } from "@/shared/utils/apiAuth"; -import { AI_MODELS } from "@/shared/constants/models"; import { getProviderConnections } from "@/lib/db/providers"; +import { getAllEmbeddingModels } from "@omniroute/open-sse/config/embeddingRegistry.ts"; import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts"; type EmbeddingModelOption = { value: string; label: string; + dimensions?: number; }; -function isLikelyEmbeddingModel(provider: string, model: string, name: string): boolean { - const haystack = `${provider}/${model} ${name}`.toLowerCase(); - if (haystack.includes("embedding")) return true; - if (haystack.includes("embed")) return true; - if (haystack.includes("text-embedding")) return true; - return false; +function modelLabel(value: string, name: string, dimensions?: number): string { + return `${value} - ${name}${dimensions ? ` (${dimensions}d)` : ""}`; } export async function GET(request: NextRequest) { @@ -23,24 +20,37 @@ export async function GET(request: NextRequest) { } try { - const options: EmbeddingModelOption[] = AI_MODELS.filter((m: any) => - isLikelyEmbeddingModel(String(m.provider || ""), String(m.model || ""), String(m.name || "")) - ) - .map((m: any) => ({ - value: `${m.provider}/${m.model}`, - label: `${m.provider}/${m.model} - ${m.name}`, + const activeConnections = (await getProviderConnections({ isActive: true })) as Array< + Record + >; + const configuredProviders = new Set( + activeConnections + .filter( + (connection) => + (typeof connection.apiKey === "string" && connection.apiKey.trim().length > 0) || + connection.authType === "oauth" + ) + .map((connection) => String(connection.provider || "")) + .filter(Boolean) + ); + + const options: EmbeddingModelOption[] = getAllEmbeddingModels() + .filter((model) => configuredProviders.has(model.provider)) + .map((model) => ({ + value: model.id, + label: modelLabel(model.id, model.name, model.dimensions), + ...(model.dimensions ? { dimensions: model.dimensions } : {}), })) - .sort((a, b) => a.value.localeCompare(b.value)); // teknik sıralama: ASCII kasıtlı + .sort((a, b) => a.value.localeCompare(b.value)); // Add OpenRouter account models that explicitly support embeddings. try { - const connections = (await getProviderConnections({ - provider: "openrouter", - isActive: true, - })) as Array>; - const apiKey = connections.find( - (c) => typeof c.apiKey === "string" && (c.apiKey as string).trim().length > 0 - )?.apiKey as string | undefined; + const apiKey = activeConnections + .filter((connection) => connection.provider === "openrouter") + .find( + (connection) => + typeof connection.apiKey === "string" && connection.apiKey.trim().length > 0 + )?.apiKey as string | undefined; if (apiKey) { const controller = new AbortController(); @@ -49,9 +59,7 @@ export async function GET(request: NextRequest) { try { res = await fetch("https://openrouter.ai/api/v1/models?output_modalities=embeddings", { method: "GET", - headers: { - Authorization: `Bearer ${apiKey}`, - }, + headers: { Authorization: `Bearer ${apiKey}` }, cache: "no-store", signal: controller.signal, }); @@ -65,11 +73,8 @@ export async function GET(request: NextRequest) { const id = typeof row?.id === "string" ? row.id.trim() : ""; if (!id) continue; const value = `openrouter/${id}`; - if (options.some((o) => o.value === value)) continue; - options.push({ - value, - label: `${value} - ${String(row?.name || id)}`, - }); + if (options.some((option) => option.value === value)) continue; + options.push({ value, label: modelLabel(value, String(row?.name || id)) }); } } } @@ -77,16 +82,7 @@ export async function GET(request: NextRequest) { // Best effort only: keep endpoint fast and resilient. } - // Ensure the default always exists as a safe fallback. - if (!options.some((o) => o.value === "openai/text-embedding-3-small")) { - options.unshift({ - value: "openai/text-embedding-3-small", - label: "openai/text-embedding-3-small - OpenAI Text Embedding 3 Small", - }); - } - - options.sort((a, b) => a.value.localeCompare(b.value)); // teknik sıralama: ASCII kasıtlı - + options.sort((a, b) => a.value.localeCompare(b.value)); return NextResponse.json({ models: options }); } catch (error) { const message = sanitizeErrorMessage(error instanceof Error ? error.message : String(error)); diff --git a/tests/integration/qdrant-routes.test.ts b/tests/integration/qdrant-routes.test.ts index 38de422c19..252c6e505f 100644 --- a/tests/integration/qdrant-routes.test.ts +++ b/tests/integration/qdrant-routes.test.ts @@ -370,10 +370,31 @@ test("GET /api/settings/qdrant/embedding-models — returns models array", async assert.strictEqual(res.status, 200); const body = await res.json(); assert.ok(Array.isArray(body.models), "should have models array"); - // Should have at least the default fallback model - assert.ok(body.models.length > 0, "should have at least one model"); + assert.strictEqual(body.models.length, 0, "should not list models without a configured provider"); +}); + +test("GET /api/settings/qdrant/embedding-models — lists only configured providers", async () => { + await localDb.createProviderConnection({ + provider: "openai", + authType: "apikey", + name: "embedding-test-openai", + apiKey: "sk-test-embedding", + }); + + const headers = await createManagementSessionHeaders(); + const req = new Request("http://localhost/api/settings/qdrant/embedding-models", { + method: "GET", + headers: Object.fromEntries(headers.entries()), + }); + + const res = await qdrantEmbeddingModelsRoute.GET(req as any); + assert.strictEqual(res.status, 200); + const body = await res.json(); + assert.ok(body.models.length > 0, "should list models for configured provider"); + assert.ok(body.models.every((model: any) => model.value.startsWith("openai/"))); + assert.ok(body.models.some((model: any) => model.value === "openai/text-embedding-3-small")); const defaultModel = body.models.find((m: any) => m.value === "openai/text-embedding-3-small"); - assert.ok(defaultModel, "should include openai/text-embedding-3-small as default"); + assert.match(defaultModel.label, /1536d/); }); test("GET /api/settings/qdrant/embedding-models — 401 without auth", async () => {