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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(db):** the Qdrant embedding-model dropdown now lists local/self-hosted providers (Ollama, LM Studio, vLLM, etc.) — an active connection is treated as "configured" when the provider allows an optional API key, not only when it has a real key or OAuth, so a running local embedding provider is no longer hidden from the picker ([#11949](https://github.com/diegosouzapw/OmniRoute/issues/11949))
8 changes: 7 additions & 1 deletion src/app/api/settings/qdrant/embedding-models/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import { getProviderConnections } from "@/lib/db/providers";
import { providerAllowsOptionalApiKey } from "@/shared/constants/providers";
import { getAllEmbeddingModels } from "@omniroute/open-sse/config/embeddingRegistry.ts";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";

Expand Down Expand Up @@ -28,7 +29,12 @@ export async function GET(request: NextRequest) {
.filter(
(connection) =>
(typeof connection.apiKey === "string" && connection.apiKey.trim().length > 0) ||
connection.authType === "oauth"
connection.authType === "oauth" ||
// Local/self-hosted providers (ollama-local, lm-studio, etc.) and other
// no-key-required providers connect with no apiKey and authType "apikey"
// (see src/app/api/providers/route.ts) — they are still "configured" the
// moment the connection is active. See issue #11949.
providerAllowsOptionalApiKey(connection.provider)
)
.map((connection) => String(connection.provider || ""))
.filter(Boolean)
Expand Down
60 changes: 60 additions & 0 deletions tests/integration/qdrant-routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ const qdrantEmbeddingModelsRoute =
// returns the Fetch API Request, which is structurally sufficient at runtime.
const asNextRequest = (req: Request) => req as unknown as import("next/server").NextRequest;

// Mirrors EmbeddingModelOption from the embedding-models route response shape.
interface EmbeddingModelOptionLike {
value: string;
label: string;
dimensions?: number;
}

async function resetStorage() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
Expand Down Expand Up @@ -409,6 +416,59 @@ test("GET /api/settings/qdrant/embedding-models — lists only configured provid
assert.match(defaultModel.label, /1536d/);
});

test("GET /api/settings/qdrant/embedding-models — includes an active local no-API-key provider (#11949)", async () => {
// Local providers (e.g. ollama-local) are created through the connections
// route with authType "apikey" but no apiKey — the connect form never asks
// for one (src/shared/constants/providers/local.ts). An active connection
// like this must still surface its embedding models.
await localDb.createProviderConnection({
provider: "ollama-local",
authType: "apikey",
name: "local-ollama",
});

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(asNextRequest(req));
assert.strictEqual(res.status, 200);
const body = await res.json();
assert.ok(
body.models.some(
(model: EmbeddingModelOptionLike) => model.value === "ollama-local/embeddinggemma"
),
"should list ollama-local embedding models for an active, key-less local connection"
);
});

test("GET /api/settings/qdrant/embedding-models — still excludes a remote provider with no key", async () => {
// A remote provider that DOES require a key must stay excluded when the
// connection was created/left without one — regression guard for the fix
// above so it does not become "drop the filter entirely".
await localDb.createProviderConnection({
provider: "openai",
authType: "apikey",
name: "embedding-test-openai-no-key",
});

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(asNextRequest(req));
assert.strictEqual(res.status, 200);
const body = await res.json();
assert.ok(
body.models.every((model: EmbeddingModelOptionLike) => !model.value.startsWith("openai/")),
"should not list a remote provider's models when its active connection has no API key"
);
});

test("GET /api/settings/qdrant/embedding-models — 401 without auth", async () => {
await setRequireLogin(true);
const req = makeUnauthRequest("GET", "http://localhost/api/settings/qdrant/embedding-models");
Expand Down