From 65ab30ca98e775305f685864c5062282e66ec2ef Mon Sep 17 00:00:00 2001 From: Markus Hartung Date: Sat, 29 Aug 2026 05:45:02 -0300 Subject: [PATCH] fix(db): include local no-API-key providers in Qdrant embedding-model list (#11949) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The embedding-models route only treated a provider connection as "configured" when it had a real apiKey or authType "oauth". Local/self-hosted providers (ollama-local, lm-studio, vllm, etc.) connect through the connections route with authType "apikey" but no apiKey — the connect form never asks for one (src/shared/constants/providers/local.ts) — so an active local connection's embedding models (embeddinggemma, nomic-embed-text, bge-m3 for ollama-local) never surfaced in the dropdown even though the connection tested "Ativo". Reuse the existing canonical providerAllowsOptionalApiKey() helper (src/shared/constants/providers.ts) — already used for the identical requiresApiKey check in src/app/api/providers/[id]/test/route.ts and across the Zod validation schemas — instead of hardcoding a provider id list. --- .../11949-qdrant-local-embedding-providers.md | 1 + .../settings/qdrant/embedding-models/route.ts | 8 ++- tests/integration/qdrant-routes.test.ts | 60 +++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 changelog.d/fixes/11949-qdrant-local-embedding-providers.md diff --git a/changelog.d/fixes/11949-qdrant-local-embedding-providers.md b/changelog.d/fixes/11949-qdrant-local-embedding-providers.md new file mode 100644 index 0000000000..b06387031e --- /dev/null +++ b/changelog.d/fixes/11949-qdrant-local-embedding-providers.md @@ -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)) diff --git a/src/app/api/settings/qdrant/embedding-models/route.ts b/src/app/api/settings/qdrant/embedding-models/route.ts index a0cd0b3441..967ac76c91 100644 --- a/src/app/api/settings/qdrant/embedding-models/route.ts +++ b/src/app/api/settings/qdrant/embedding-models/route.ts @@ -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"; @@ -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) diff --git a/tests/integration/qdrant-routes.test.ts b/tests/integration/qdrant-routes.test.ts index 451074ffea..bc6c291adc 100644 --- a/tests/integration/qdrant-routes.test.ts +++ b/tests/integration/qdrant-routes.test.ts @@ -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 }); @@ -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");