feat: list embeddings from configured providers (#11249)

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!
This commit is contained in:
Rafa Martins
2026-08-23 14:42:09 -03:00
committed by GitHub
parent d077e88456
commit 527da6565d
2 changed files with 59 additions and 42 deletions

View File

@@ -1,20 +1,17 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { isAuthenticated } from "@/shared/utils/apiAuth"; import { isAuthenticated } from "@/shared/utils/apiAuth";
import { AI_MODELS } from "@/shared/constants/models";
import { getProviderConnections } from "@/lib/db/providers"; import { getProviderConnections } from "@/lib/db/providers";
import { getAllEmbeddingModels } from "@omniroute/open-sse/config/embeddingRegistry.ts";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts"; import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
type EmbeddingModelOption = { type EmbeddingModelOption = {
value: string; value: string;
label: string; label: string;
dimensions?: number;
}; };
function isLikelyEmbeddingModel(provider: string, model: string, name: string): boolean { function modelLabel(value: string, name: string, dimensions?: number): string {
const haystack = `${provider}/${model} ${name}`.toLowerCase(); return `${value} - ${name}${dimensions ? ` (${dimensions}d)` : ""}`;
if (haystack.includes("embedding")) return true;
if (haystack.includes("embed")) return true;
if (haystack.includes("text-embedding")) return true;
return false;
} }
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
@@ -23,24 +20,37 @@ export async function GET(request: NextRequest) {
} }
try { try {
const options: EmbeddingModelOption[] = AI_MODELS.filter((m: any) => const activeConnections = (await getProviderConnections({ isActive: true })) as Array<
isLikelyEmbeddingModel(String(m.provider || ""), String(m.model || ""), String(m.name || "")) Record<string, unknown>
) >;
.map((m: any) => ({ const configuredProviders = new Set(
value: `${m.provider}/${m.model}`, activeConnections
label: `${m.provider}/${m.model} - ${m.name}`, .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. // Add OpenRouter account models that explicitly support embeddings.
try { try {
const connections = (await getProviderConnections({ const apiKey = activeConnections
provider: "openrouter", .filter((connection) => connection.provider === "openrouter")
isActive: true, .find(
})) as Array<Record<string, unknown>>; (connection) =>
const apiKey = connections.find( typeof connection.apiKey === "string" && connection.apiKey.trim().length > 0
(c) => typeof c.apiKey === "string" && (c.apiKey as string).trim().length > 0 )?.apiKey as string | undefined;
)?.apiKey as string | undefined;
if (apiKey) { if (apiKey) {
const controller = new AbortController(); const controller = new AbortController();
@@ -49,9 +59,7 @@ export async function GET(request: NextRequest) {
try { try {
res = await fetch("https://openrouter.ai/api/v1/models?output_modalities=embeddings", { res = await fetch("https://openrouter.ai/api/v1/models?output_modalities=embeddings", {
method: "GET", method: "GET",
headers: { headers: { Authorization: `Bearer ${apiKey}` },
Authorization: `Bearer ${apiKey}`,
},
cache: "no-store", cache: "no-store",
signal: controller.signal, signal: controller.signal,
}); });
@@ -65,11 +73,8 @@ export async function GET(request: NextRequest) {
const id = typeof row?.id === "string" ? row.id.trim() : ""; const id = typeof row?.id === "string" ? row.id.trim() : "";
if (!id) continue; if (!id) continue;
const value = `openrouter/${id}`; const value = `openrouter/${id}`;
if (options.some((o) => o.value === value)) continue; if (options.some((option) => option.value === value)) continue;
options.push({ options.push({ value, label: modelLabel(value, String(row?.name || id)) });
value,
label: `${value} - ${String(row?.name || id)}`,
});
} }
} }
} }
@@ -77,16 +82,7 @@ export async function GET(request: NextRequest) {
// Best effort only: keep endpoint fast and resilient. // Best effort only: keep endpoint fast and resilient.
} }
// Ensure the default always exists as a safe fallback. options.sort((a, b) => a.value.localeCompare(b.value));
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ı
return NextResponse.json({ models: options }); return NextResponse.json({ models: options });
} catch (error) { } catch (error) {
const message = sanitizeErrorMessage(error instanceof Error ? error.message : String(error)); const message = sanitizeErrorMessage(error instanceof Error ? error.message : String(error));

View File

@@ -370,10 +370,31 @@ test("GET /api/settings/qdrant/embedding-models — returns models array", async
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const body = await res.json(); const body = await res.json();
assert.ok(Array.isArray(body.models), "should have models array"); assert.ok(Array.isArray(body.models), "should have models array");
// Should have at least the default fallback model assert.strictEqual(body.models.length, 0, "should not list models without a configured provider");
assert.ok(body.models.length > 0, "should have at least one model"); });
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"); 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 () => { test("GET /api/settings/qdrant/embedding-models — 401 without auth", async () => {