Files
OmniRoute/docs/routing/MODEL_EXPOSURE_LIST.md
Diego Rodrigues de Sa e Souza 34e2f84c04 feat(api): explicit model exposure allow/deny list for /v1/models (#11481) (#11997)
Adds opt-in modelVisibilityAllowlist/modelVisibilityDenylist settings so an operator can curate exactly which models GET /v1/models advertises, mirrored into auto/* combo candidate pools (the same trap #6512 fixed for hidePaidModels). Default off, no behavior change for anyone who doesn't opt in.

TDD: 4 new test files, 22/22 passing (16 node:test + 6 vitest) + regression sweep across virtual-auto-combo/hide-paid/hide-auto-no-think suites (21/21).

Rebased onto the updated tip (a sibling #9133 landed first, same file) — kept both rebaseline annotations in file-size-baseline.json and set the value to the real measured line count after both merged.
2026-08-29 15:40:32 -03:00

3.7 KiB

title, version, lastUpdated
title version lastUpdated
Model Exposure Allow/Deny List 3.8.51 2026-08-29

Model Exposure Allow/Deny List

Opt-in, off by default (modelVisibilityAllowlist/modelVisibilityDenylist both empty leaves the /v1/models catalog AND every auto/* candidate pool byte-identical). A sibling of hidePaidModels/hideAutoCombos (src/lib/db/settings.ts) for operators who want a curated model list for reasons that have nothing to do with cost.

Why this exists

hidePaidModels answers "is this model free?" and hideAutoCombos answers "should auto/* virtual ids be advertised at all?" — neither lets an operator curate an arbitrary subset of models (e.g. presenting exactly the models a given Claude Code / OpenCode client should see, independent of pricing). #11481 adds that as two independent, opt-in string-array settings.

Settings

Key Type Default Meaning
modelVisibilityDenylist string[] [] Entries matching a candidate hide it from the catalog/candidate pool.
modelVisibilityAllowlist string[] [] When non-empty, ONLY entries matching a candidate stay exposed.

Both accept up to 500 entries of up to 200 characters each (Zod-validated in src/shared/validation/settingsSchemas.ts). An entry is either:

  • an exact catalog id — "gpt-4o" (bare model id) or "openai/gpt-4o" (provider-prefixed), or
  • a glob pattern using */? — e.g. "openai/gpt-4*" or "anthropic/*" — resolved via the same shared globToRegex() matcher (src/shared/utils/globPattern.ts) already used by ModelRoutingSection's per-model combo mappings and freeModels.ts::matchesOnlyPaidModels.

Precedence: the denylist is checked first (a denied entry is always hidden, even if it also matches the allowlist); when the allowlist is non-empty, only entries it matches survive.

Two chokepoints, not one

The lesson from #6512 (a hidePaidModels-only catalog filter still let auto/* route to a paid model, since the combo candidate pool was built independently) applies identically here. The matching predicate isModelExposureAllowed() (src/shared/utils/modelExposureList.ts) is called from BOTH:

  • src/app/api/v1/models/catalog.ts — the /v1/models listing itself, at the same 5 per-source chokepoints shouldHidePaid() already gates (static PROVIDER_MODELS, synced provider rows, custom rows, alias-backed rows, managed-fallback rows).
  • open-sse/services/autoCombo/modelExposureFilter.ts::filterModelExposureCandidates() — called from virtualFactory.ts::buildPreparedPool, immediately after the equivalent filterPaidOnlyCandidates() call, so a denied model can never be selected into an auto/* candidate pool either.

What is NOT filtered

Mirrors hideAutoCombos's existing behaviour: a model id sent explicitly (not via auto/*, and not discovered through the catalog listing) is never blocked at dispatch — only advertisement/candidate-pool membership is filtered. This is independent of hidePaidModels; an operator may want a curated set for reasons that have nothing to do with cost, so both settings compose as independent AND-ed filters, same as the existing multi-flag composition in catalog.ts.

Settings export (GET /api/settings/export-json) includes both arrays verbatim, like any other settings field — unlike hidePaidModels's combo-step export filter, there is no re-hydration risk here: a denied id embedded in an exported combo step is the operator's own explicit routing choice, not something the export boundary needs to strip.