mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-12 02:02:13 +03:00
fix(api): specialty model catalog ignores hidden OpenRouter model flags (#9293)
The specialty model catalog loops (image, rerank, audio, moderation, video,
music) in catalog.ts reduced OpenRouter model IDs to only the final path
segment via .split("/").pop() before calling getModelIsHidden(), so stored
hidden flags with full provider-relative paths (e.g. openrouter+google/chirp-3)
were never matched.
Fix: introduce a shared getSpecialtyModelRelativeId helper that strips only
the provider prefix (like the embedding loop already did), and apply it to
all 6 affected specialty loops. Also add a hidden-model guard to the live
OpenRouter catalog path that had no such check at all.
This commit is contained in:
1
changelog.d/fixes/9293-fix.plan.md
Normal file
1
changelog.d/fixes/9293-fix.plan.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(api): specialty model catalog ignores hidden OpenRouter model flags (#9293)
|
||||
@@ -959,6 +959,9 @@ async function buildUnifiedModelsResponseCore(
|
||||
const modelType = getOpenRouterModelType(inputModalities, outputModalities);
|
||||
const isFree = isOpenRouterFreeModel(openRouterModel);
|
||||
if (hidePaid && !isFree) continue;
|
||||
// #9293: respect per-model hidden flags (e.g. operator hid google/chirp-3
|
||||
// from the OpenRouter provider, so it should not appear in the live catalog).
|
||||
if (getModelIsHidden("openrouter", openRouterModel.id)) continue;
|
||||
const supportedParameters = Array.isArray(openRouterModel.supported_parameters)
|
||||
? openRouterModel.supported_parameters
|
||||
: [];
|
||||
@@ -1041,12 +1044,20 @@ async function buildUnifiedModelsResponseCore(
|
||||
return existingRoot === rawModelId;
|
||||
});
|
||||
|
||||
// Helper: strip the provider prefix from a specialty model ID to get the
|
||||
// provider-relative path (e.g. "openrouter/google/chirp-3" -> "google/chirp-3").
|
||||
// This is the correct key used by getModelIsHidden() — using .split("/").pop()
|
||||
// here would discard all but the last segment and miss stored flags for
|
||||
// providers whose model IDs carry a sub-path (e.g. OpenRouter scoped models).
|
||||
const getSpecialtyModelRelativeId = (modelId: string, provider: string): string =>
|
||||
modelId.startsWith(`${provider}/`)
|
||||
? modelId.slice(provider.length + 1)
|
||||
: modelId;
|
||||
|
||||
// Add embedding models (filtered by active providers)
|
||||
for (const embModel of getAllEmbeddingModels()) {
|
||||
if (!isProviderActive(embModel.provider)) continue;
|
||||
const rawModelId = embModel.id.startsWith(`${embModel.provider}/`)
|
||||
? embModel.id.slice(embModel.provider.length + 1)
|
||||
: embModel.id;
|
||||
const rawModelId = getSpecialtyModelRelativeId(embModel.id, embModel.provider);
|
||||
if (!providerSupportsModel(embModel.provider, rawModelId)) continue;
|
||||
if (getModelIsHidden(embModel.provider, rawModelId)) continue;
|
||||
if (hasEquivalentSpecialtyModel(embModel.provider, rawModelId, "embedding", embModel.id)) {
|
||||
@@ -1066,7 +1077,7 @@ async function buildUnifiedModelsResponseCore(
|
||||
// Add image models (filtered by active providers)
|
||||
for (const imgModel of getAllImageModels()) {
|
||||
if (!isProviderActive(imgModel.provider)) continue;
|
||||
const rawModelId = imgModel.id.split("/").pop() || imgModel.id;
|
||||
const rawModelId = getSpecialtyModelRelativeId(imgModel.id, imgModel.provider);
|
||||
if (!providerSupportsModel(imgModel.provider, rawModelId)) continue;
|
||||
if (getModelIsHidden(imgModel.provider, rawModelId)) continue;
|
||||
models.push({
|
||||
@@ -1085,7 +1096,7 @@ async function buildUnifiedModelsResponseCore(
|
||||
// Add rerank models (filtered by active providers)
|
||||
for (const rerankModel of getAllRerankModels()) {
|
||||
if (!isProviderActive(rerankModel.provider)) continue;
|
||||
const rawModelId = rerankModel.id.split("/").pop() || rerankModel.id;
|
||||
const rawModelId = getSpecialtyModelRelativeId(rerankModel.id, rerankModel.provider);
|
||||
if (!providerSupportsModel(rerankModel.provider, rawModelId)) continue;
|
||||
if (getModelIsHidden(rerankModel.provider, rawModelId)) continue;
|
||||
if (hasEquivalentSpecialtyModel(rerankModel.provider, rawModelId, "rerank", rerankModel.id)) {
|
||||
@@ -1104,7 +1115,7 @@ async function buildUnifiedModelsResponseCore(
|
||||
// Add audio models (filtered by active providers)
|
||||
for (const audioModel of getAllAudioModels()) {
|
||||
if (!isProviderActive(audioModel.provider)) continue;
|
||||
const rawModelId = audioModel.id.split("/").pop() || audioModel.id;
|
||||
const rawModelId = getSpecialtyModelRelativeId(audioModel.id, audioModel.provider);
|
||||
if (!providerSupportsModel(audioModel.provider, rawModelId)) continue;
|
||||
if (getModelIsHidden(audioModel.provider, rawModelId)) continue;
|
||||
models.push({
|
||||
@@ -1120,7 +1131,7 @@ async function buildUnifiedModelsResponseCore(
|
||||
// Add moderation models (filtered by active providers)
|
||||
for (const modModel of getAllModerationModels()) {
|
||||
if (!isProviderActive(modModel.provider)) continue;
|
||||
const rawModelId = modModel.id.split("/").pop() || modModel.id;
|
||||
const rawModelId = getSpecialtyModelRelativeId(modModel.id, modModel.provider);
|
||||
if (!providerSupportsModel(modModel.provider, rawModelId)) continue;
|
||||
if (getModelIsHidden(modModel.provider, rawModelId)) continue;
|
||||
models.push({
|
||||
@@ -1135,7 +1146,7 @@ async function buildUnifiedModelsResponseCore(
|
||||
// Add video models (filtered by active providers)
|
||||
for (const videoModel of getAllVideoModels()) {
|
||||
if (!isProviderActive(videoModel.provider)) continue;
|
||||
const rawModelId = videoModel.id.split("/").pop() || videoModel.id;
|
||||
const rawModelId = getSpecialtyModelRelativeId(videoModel.id, videoModel.provider);
|
||||
if (!providerSupportsModel(videoModel.provider, rawModelId)) continue;
|
||||
if (getModelIsHidden(videoModel.provider, rawModelId)) continue;
|
||||
models.push({
|
||||
@@ -1150,7 +1161,7 @@ async function buildUnifiedModelsResponseCore(
|
||||
// Add music models (filtered by active providers)
|
||||
for (const musicModel of getAllMusicModels()) {
|
||||
if (!isProviderActive(musicModel.provider)) continue;
|
||||
const rawModelId = musicModel.id.split("/").pop() || musicModel.id;
|
||||
const rawModelId = getSpecialtyModelRelativeId(musicModel.id, musicModel.provider);
|
||||
if (!providerSupportsModel(musicModel.provider, rawModelId)) continue;
|
||||
if (getModelIsHidden(musicModel.provider, rawModelId)) continue;
|
||||
models.push({
|
||||
|
||||
128
tests/unit/specialty-model-hidden-openrouter-9293.test.ts
Normal file
128
tests/unit/specialty-model-hidden-openrouter-9293.test.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* #9293 — specialty model catalog ignores hidden OpenRouter model flags.
|
||||
*
|
||||
* The specialty model loops (image, rerank, audio, moderation, video, music)
|
||||
* in catalog.ts reduce OpenRouter model IDs to only the final path segment
|
||||
* via .split("/").pop() before calling getModelIsHidden(), so stored hidden
|
||||
* flags with full provider-relative paths (e.g. openrouter+google/chirp-3)
|
||||
* are never matched. The embedding loop correctly strips only the provider prefix
|
||||
* rather than taking the last segment.
|
||||
*
|
||||
* This test: seeds an OpenRouter connection, hides two OpenRouter specialty
|
||||
* models (audio: google/chirp-3, image: black-forest-labs/flux.2-pro), then
|
||||
* verifies the hidden models are excluded from the /v1/models catalog while
|
||||
* non-hidden models still appear.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-9293-specialty-hidden-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "9293-test-secret";
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const providersDb = await import("../../src/lib/db/providers.ts");
|
||||
const apiKeysDb = await import("../../src/lib/db/apiKeys.ts");
|
||||
const { mergeModelCompatOverride, getModelIsHidden } = await import("../../src/lib/localDb.ts");
|
||||
const v1ModelsCatalog = await import("../../src/app/api/v1/models/catalog.ts");
|
||||
|
||||
async function resetStorage() {
|
||||
core.resetDbInstance();
|
||||
apiKeysDb.resetApiKeyState();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
v1ModelsCatalog.__resetCatalogBuilderRunsForTest();
|
||||
}
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await resetStorage();
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
core.resetDbInstance();
|
||||
apiKeysDb.resetApiKeyState();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("#9293 hidden OpenRouter specialty models are excluded from /v1/models catalog", async () => {
|
||||
// Create an active OpenRouter connection
|
||||
const connection = await providersDb.createProviderConnection({
|
||||
provider: "openrouter",
|
||||
authType: "apikey",
|
||||
name: "openrouter-test",
|
||||
apiKey: "sk-or-test-9293",
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
providerSpecificData: {},
|
||||
});
|
||||
assert.ok(connection?.id, "OpenRouter connection created");
|
||||
|
||||
// Confirm the hidden flag is not set yet
|
||||
assert.equal(
|
||||
getModelIsHidden("openrouter", "google/chirp-3"),
|
||||
false,
|
||||
"chirp-3 is initially visible"
|
||||
);
|
||||
assert.equal(
|
||||
getModelIsHidden("openrouter", "black-forest-labs/flux.2-pro"),
|
||||
false,
|
||||
"flux.2-pro is initially visible"
|
||||
);
|
||||
|
||||
// Hide two OpenRouter specialty models: one audio, one image
|
||||
mergeModelCompatOverride("openrouter", "google/chirp-3", { isHidden: true });
|
||||
mergeModelCompatOverride("openrouter", "black-forest-labs/flux.2-pro", { isHidden: true });
|
||||
|
||||
// Confirm the hidden flags are stored correctly
|
||||
assert.equal(getModelIsHidden("openrouter", "google/chirp-3"), true, "chirp-3 is now hidden");
|
||||
assert.equal(
|
||||
getModelIsHidden("openrouter", "black-forest-labs/flux.2-pro"),
|
||||
true,
|
||||
"flux.2-pro is now hidden"
|
||||
);
|
||||
|
||||
// Fetch the full catalog
|
||||
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
|
||||
new Request("http://localhost/v1/models")
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
const body = (await response.json()) as any;
|
||||
assert.ok(Array.isArray(body.data), "response has data array");
|
||||
|
||||
// Find audio and image models
|
||||
const audioModels = body.data.filter((m: any) => m.type === "audio");
|
||||
const imageModels = body.data.filter((m: any) => m.type === "image");
|
||||
|
||||
// chirp-3 model ID from the audio registry is openrouter/google/chirp-3
|
||||
const hiddenAudio = audioModels.find((m: any) =>
|
||||
String(m.id).endsWith("google/chirp-3")
|
||||
);
|
||||
assert.equal(
|
||||
hiddenAudio,
|
||||
undefined,
|
||||
"#9293 RED: hidden audio model openrouter/google/chirp-3 should NOT appear in catalog"
|
||||
);
|
||||
|
||||
// flux.2-pro model ID from the image registry is openrouter/black-forest-labs/flux.2-pro
|
||||
const hiddenImage = imageModels.find((m: any) =>
|
||||
String(m.id).endsWith("black-forest-labs/flux.2-pro")
|
||||
);
|
||||
assert.equal(
|
||||
hiddenImage,
|
||||
undefined,
|
||||
"#9293 RED: hidden image model openrouter/black-forest-labs/flux.2-pro should NOT appear in catalog"
|
||||
);
|
||||
|
||||
// Verify non-hidden audio models from OpenRouter still appear
|
||||
// deepgram/nova-3 is not hidden, so it should be present
|
||||
const visibleAudio = audioModels.find((m: any) =>
|
||||
String(m.id).endsWith("deepgram/nova-3")
|
||||
);
|
||||
assert.ok(
|
||||
visibleAudio,
|
||||
"non-hidden audio model deepgram/nova-3 should still appear in catalog"
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user