fix(api): /v1beta/models lists only active-connection providers (#2483)

This commit is contained in:
diegosouzapw
2026-05-21 10:56:15 -03:00
parent d01b1bd762
commit 1d6077f8cf
2 changed files with 68 additions and 2 deletions

View File

@@ -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<Set<string>> {
const keys = new Set<string>();
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<string, string>)[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;

View File

@@ -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"
);
});