fix(routing): honor eye-icon hidden models for no-auth providers in auto-combo (#7620) (#7750)

getNoAuthCandidates() in open-sse/services/autoCombo/virtualFactory.ts built the
candidate pool for no-auth providers (opencode/mimocode/etc.) without ever
consulting getHiddenModelsByProvider(), unlike the credentialed-connection loop
a few lines above it. A model hidden via the dashboard eye icon stayed in every
auto/* candidate pool forever and could still be selected.

Wire hiddenModelsMap into getNoAuthCandidates() the same way #7622 wired
noAuthProviderSpecificData in, mirroring the existing credentialed-connection
check.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-19 09:39:08 -03:00
committed by GitHub
parent c95a161709
commit ded4ac830e
3 changed files with 87 additions and 2 deletions

View File

@@ -0,0 +1 @@
- fix(routing): honor eye-icon hidden models for no-auth providers in auto-combo candidate pools (#7620)

View File

@@ -140,7 +140,8 @@ function getNoAuthCandidates(
excludedProviders: Set<string>,
blockedProviders: Set<string>,
disabledNoAuthProviders: Set<string>,
noAuthProviderSpecificData: Map<string, Record<string, unknown> | null | undefined>
noAuthProviderSpecificData: Map<string, Record<string, unknown> | null | undefined>,
hiddenModelsMap: Map<string, Set<string>>
): VirtualAutoComboCandidate[] {
const registry = getProviderRegistry();
const candidates: VirtualAutoComboCandidate[] = [];
@@ -190,10 +191,19 @@ function getNoAuthCandidates(
? noAuthProviderSpecificData.get(providerDef.alias)
: undefined);
// #7620: honor the eye-icon "hidden" flag (isHidden, from the
// modelCompatOverrides/customModels key_value namespaces) the same way the
// credentialed-connection loop below does, so a hidden no-auth model never
// enters the auto-combo/fusion candidate pool either.
const hiddenModels =
hiddenModelsMap.get(providerId) ??
(typeof providerDef.alias === "string" ? hiddenModelsMap.get(providerDef.alias) : undefined);
for (const model of registryModels) {
const modelId = typeof model?.id === "string" && model.id.trim().length > 0 ? model.id : null;
if (!modelId) continue;
if (isModelExcludedByConnection(modelId, providerSpecificData)) continue;
if (hiddenModels?.has(modelId)) continue;
candidates.push({
provider: providerId,
connectionId: SYNTHETIC_NOAUTH_CONNECTION_ID,
@@ -323,7 +333,8 @@ export async function createVirtualAutoCombo(
new Set(validConnections.map((conn) => conn.provider)),
blockedProviders,
disabledNoAuthProviders,
noAuthProviderSpecificData
noAuthProviderSpecificData,
hiddenModelsMap
)
);

View File

@@ -0,0 +1,73 @@
/**
* #7620 — hiding a no-auth-provider model with the EYE icon (Dashboard → Models,
* `isHidden: true` written via setModelIsHidden()/mergeModelCompatOverride()) does
* remove it from `/v1/models`, but `getNoAuthCandidates()` in
* `open-sse/services/autoCombo/virtualFactory.ts` never consults
* `getHiddenModelsByProvider()` at all (unlike the credentialed-connection loop a
* few lines above it, which does). A hidden no-auth model therefore stays in the
* `auto/*` candidate pool and can still be selected, causing a 401 when the
* upstream account for that hidden model is no longer valid/allowed.
*/
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-7620-noauth-hidden-"));
const ORIGINAL_DATA_DIR = process.env.DATA_DIR;
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const modelsDb = await import("../../src/lib/db/models.ts");
const virtualFactory = await import("../../open-sse/services/autoCombo/virtualFactory.ts");
async function resetStorage() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.beforeEach(async () => {
await resetStorage();
});
test.after(async () => {
await resetStorage();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
if (ORIGINAL_DATA_DIR === undefined) {
delete process.env.DATA_DIR;
} else {
process.env.DATA_DIR = ORIGINAL_DATA_DIR;
}
});
test("#7620: a no-auth model hidden via the eye icon (isHidden:true) must be ABSENT from the auto-combo candidate pool", async () => {
modelsDb.setModelIsHidden("opencode", "mimo-v2.5-free", true);
const hiddenMap = modelsDb.getHiddenModelsByProvider();
assert.equal(
hiddenMap.get("opencode")?.has("mimo-v2.5-free"),
true,
"sanity: getHiddenModelsByProvider() must report opencode/mimo-v2.5-free as hidden"
);
const combo = await virtualFactory.createVirtualAutoCombo(undefined);
const modelStrings = combo.models.map((m: { model: string }) => m.model);
assert.ok(
!modelStrings.some((model: string) => model.endsWith("/mimo-v2.5-free")),
"BUG #7620: the eye-hidden model 'mimo-v2.5-free' must not appear in the auto-combo " +
`candidate pool, but it did. Pool: ${JSON.stringify(modelStrings)}`
);
});
test("#7620 baseline: with nothing hidden, opencode/mimo-v2.5-free is present in the pool", async () => {
const combo = await virtualFactory.createVirtualAutoCombo(undefined);
const modelStrings = combo.models.map((m: { model: string }) => m.model);
assert.ok(
modelStrings.some((model: string) => model.endsWith("/mimo-v2.5-free")),
`baseline: with nothing hidden, mimo-v2.5-free must be present. Pool: ${JSON.stringify(modelStrings)}`
);
});