mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 13:52:09 +03:00
fix(api): /v1beta/models lists only active-connection providers (#2483)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user