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 { 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<string, unknown>
>;
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<Record<string, unknown>>;
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));

View File

@@ -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 () => {