From 1d6077f8cfec8c34f314f99ecdaa89ed3b4b0cb0 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 21 May 2026 10:56:15 -0300 Subject: [PATCH] fix(api): /v1beta/models lists only active-connection providers (#2483) --- src/app/api/v1beta/models/route.ts | 37 ++++++++++++++++++++++++-- tests/unit/v1beta-models-route.test.ts | 33 +++++++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/app/api/v1beta/models/route.ts b/src/app/api/v1beta/models/route.ts index 8d6a50834b..7e782d6e8e 100644 --- a/src/app/api/v1beta/models/route.ts +++ b/src/app/api/v1beta/models/route.ts @@ -1,13 +1,38 @@ -import { PROVIDER_MODELS } from "@/shared/constants/models"; +import { PROVIDER_MODELS, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models"; import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error"; import { getAllCustomModels, getAllSyncedAvailableModels, getSyncedAvailableModels, } from "@/lib/db/models"; +import { getProviderConnections } from "@/lib/localDb"; import { getResolvedModelCapabilities } from "@/lib/modelCapabilities"; import { getSyncedCapabilities } from "@/lib/modelsDevSync"; +/** + * Build the set of provider keys (raw id + alias) that have at least one active/validated + * connection. Mirrors the active-provider filter used by the OpenAI-format /v1/models + * catalog so /v1beta/models only lists models the user can actually call (#2483). + */ +async function getActiveProviderKeys(): Promise> { + const keys = new Set(); + try { + const connections = await getProviderConnections(); + for (const conn of connections) { + if (conn.isActive === false) continue; + const provider = conn.provider; + if (!provider) continue; + keys.add(provider); + const alias = (PROVIDER_ID_TO_ALIAS as Record)[provider]; + if (alias) keys.add(alias); + } + } catch (e) { + // DB unavailable — return empty set (safe default: list nothing provider-gated) + console.error("[v1beta/models] Could not fetch provider connections:", e); + } + return keys; +} + /** * Handle CORS preflight */ @@ -29,8 +54,12 @@ export async function GET() { getSyncedCapabilities(); const models = []; + // Only list models whose provider has an active/validated connection (#2483). + const activeKeys = await getActiveProviderKeys(); + // Built-in models (hardcoded defaults) for (const [provider, providerModels] of Object.entries(PROVIDER_MODELS)) { + if (!activeKeys.has(provider)) continue; for (const model of providerModels) { const resolved = getResolvedModelCapabilities({ provider, model: model.id }); models.push({ @@ -56,7 +85,9 @@ export async function GET() { } } try { - const syncedGeminiModels = await getSyncedAvailableModels("gemini"); + const syncedGeminiModels = activeKeys.has("gemini") + ? await getSyncedAvailableModels("gemini") + : []; for (const m of syncedGeminiModels) { models.push({ name: `models/gemini/${m.id}`, @@ -79,6 +110,7 @@ export async function GET() { const syncedModelsMap = await getAllSyncedAvailableModels(); for (const [providerId, syncedModels] of Object.entries(syncedModelsMap)) { if (providerId === "gemini") continue; + if (!activeKeys.has(providerId)) continue; if (!Array.isArray(syncedModels)) continue; for (const m of syncedModels) { if (!m || typeof m.id !== "string") continue; @@ -119,6 +151,7 @@ export async function GET() { if (!Array.isArray(rawModels)) continue; // Skip Gemini — handled by syncedAvailableModels above if (providerId === "gemini") continue; + if (!activeKeys.has(providerId)) continue; for (const model of rawModels) { if (!model || typeof model !== "object" || typeof (model as any).id !== "string") continue; diff --git a/tests/unit/v1beta-models-route.test.ts b/tests/unit/v1beta-models-route.test.ts index 21f6b210b4..1f66e7ae24 100644 --- a/tests/unit/v1beta-models-route.test.ts +++ b/tests/unit/v1beta-models-route.test.ts @@ -10,8 +10,18 @@ process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "v1beta-models-test-s const core = await import("../../src/lib/db/core.ts"); const modelsDb = await import("../../src/lib/db/models.ts"); +const providersDb = await import("../../src/lib/db/providers.ts"); const v1betaModelsRoute = await import("../../src/app/api/v1beta/models/route.ts"); +async function addActiveConnection(provider: string) { + await providersDb.createProviderConnection({ + provider, + authType: "apikey", + apiKey: `test-key-${provider}`, + testStatus: "active", + }); +} + async function resetStorage() { core.resetDbInstance(); fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); @@ -28,6 +38,8 @@ test.after(async () => { }); test("v1beta models route deduplicates custom models against built-in and synced entries", async () => { + // #2483: the route now lists only models whose provider has an active connection. + await addActiveConnection("openai"); await modelsDb.replaceSyncedAvailableModelsForConnection("openai", "conn-main", [ { id: "gpt-4o", @@ -53,3 +65,24 @@ test("v1beta models route deduplicates custom models against built-in and synced assert.equal(names.filter((name) => name === "models/openai/review-sync-only").length, 1); assert.equal(names.filter((name) => name === "models/openai/review-manual-only").length, 1); }); + +test("v1beta models route excludes providers without an active connection (#2483)", async () => { + // No connections configured at all → no built-in catalog models should leak. + const emptyResp = await v1betaModelsRoute.GET(); + const emptyBody = (await emptyResp.json()) as { models: Array<{ name: string }> }; + assert.equal(emptyResp.status, 200); + assert.equal(emptyBody.models.length, 0, "no active connections → empty model list"); + + // Configure ONLY an anthropic connection; custom models for an unconfigured provider + // (kie) must NOT appear, while anthropic catalog models do. + await addActiveConnection("anthropic"); + await modelsDb.addCustomModel("kie", "claude-opus-4-7", "Kie Claude Opus"); + const resp = await v1betaModelsRoute.GET(); + const body = (await resp.json()) as { models: Array<{ name: string }> }; + const names = body.models.map((m) => m.name); + assert.ok(!names.some((n) => n.startsWith("models/kie/")), "unconfigured kie must be excluded"); + assert.ok( + names.some((n) => n.startsWith("models/anthropic/")), + "configured anthropic must be present" + ); +});