Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 35 additions & 39 deletions src/app/api/settings/qdrant/embedding-models/route.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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();
Expand All @@ -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,
});
Expand All @@ -65,28 +73,16 @@ 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)) });
}
}
}
} catch {
// 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));
Expand Down
27 changes: 24 additions & 3 deletions tests/integration/qdrant-routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
Loading