mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-24 16:12:23 +03:00
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:
@@ -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));
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user