Repository navigation
Description
Feature hasn't been suggested before.
- I have verified this feature I'm about to request hasn't been suggested before.
Note on the checkbox: this revives #44070, which was closed as
not_plannedby the
60-day stale bot without a maintainer response — the bot's closing comment invites
opening a new issue if the request is still relevant. It is scoped to the
policy-system angle and is meant to complement, not duplicate, #49986 and PR #54372,
which address the same need through provider config.
Describe the enhancement
Enterprise context
In managed deployments, governance boundaries do not align with provider boundaries:
- An internal OpenAI-compatible gateway commonly proxies many models — internally hosted
and third-party/public — under a single custom provider ID (one endpoint, one
credential). - Compliance classes are per model (internal vs. public, regional/data-residency),
not per provider. Teams need "this project may only use these approved models", where
the approved models are a subset of one shared provider. - The same underlying model is often reachable through several providers (the gateway
and public providers), so provider-level rules cannot express "block this model
everywhere".
Current behavior (verified on v2.2.5)
- The policy actions are
provider.use,tool.use,integration.connect,
integration.use,mcp.use,skill.use,plugin.use. There is no model-level
action;provider.useresources match provider IDs only, and a denied provider is
removed from the server registry. - The only model-level control is
providers.<id>.models.<model>.disabled: true— an
ordinary config setting with deny-list semantics: a model that appears on an
allowed provider later (catalog update, config change, dynamic discovery) is usable
until someone disables it (fail-open). - V1
whitelist/blacklistare dropped with an "omitted unsupported legacy setting"
diagnostic (documented under "Accepted but unsupported fields").
Why the policy system, and not only provider config
PR #54372 proposes providers.<id>.models["*"]: { "disabled": true } plus per-model
exceptions. That solves the fail-open problem at the config layer and would already
cover our project-level use case — thank you for it. A model-level policy would
additionally provide the properties the docs reserve for policies:
- The migration guide says admin-enforced restrictions belong in policies:
"Overriding a setting cannot stop users from adding providers, MCP servers, agents,
or plugins, so express admin-enforced restrictions aspoliciesrather than ordinary
settings." Models are currently the only governed resource class (providers, tools,
integrations, MCP, skills, plugins) without a policy action. - Policies "only ever tighten" and evaluate with reversed precedence — managed config,
system config, and a connected Console outrank user and project files. Admins get the
same enforcement guarantees for models that they already have for providers. - Policy applies however a provider became known (catalog data, environment variables,
saved accounts, plugins, configuration) — a single enforcement point instead of
per-source config overrides. - Rules by model identity across providers (e.g. block a model family on every
provider) become expressible.
Proposal (either shape, maintainer's choice)
Option A — let provider.use resources match providerID/modelID:
{
"policies": [
{ "action": "provider.use", "resource": "*", "effect": "deny" },
{ "action": "provider.use", "resource": "gateway", "effect": "allow" },
{ "action": "provider.use", "resource": "gateway/*", "effect": "deny" },
{ "action": "provider.use", "resource": "gateway/internal-model-a", "effect": "allow" }
]
}With last-match-wins this composes with existing statements: only
gateway/internal-model-a remains usable; it disappears from the catalog and explicit
--model selection fails — consistent with how denied providers behave today.
Option B — a dedicated model.use action with provider/model resources, as
originally proposed in #44070.
Both should reuse the existing whole-value wildcard matching (*, ?) and the existing
policy precedence rules.
Workaround in use today (verified)
Each project defines a "virtual" custom provider that re-declares only the approved
models (same baseURL and credential as the real gateway provider) and allows only that
provider via provider.use. It works and is fail-closed, but it duplicates the
endpoint, credential, and model definitions in every project's config, and drifts when
the gateway or its catalog changes. A native model-level policy (or the #54372
wildcard) removes that duplication.
Related
- [FEATURE]: [V2] Add Model Allowlisting Support #44070 — original
model.useproposal (stale-closed; revived here) - [FEATURE]: Add a native V2 replacement for provider model filtering #49986 — open feature request: native V2 replacement for V1
whitelist/blacklist - fix(core): migrate provider whitelist and blacklist to model overrides #54372 — open PR:
models["*"]provider-widedisableddefault (config-level, complementary) - [FEATURE]: Allow Go plan users to restrict which models the client can enable #41357, v2:
experimental.policiesnot enforced for model selection or LLM execution #34971, [FEATURE/BUG] OpenCode Zen disabled models still appear in TUI picker (config drift risk) #40560 — related restriction/enforcement reports - provider.<id> whitelist/blacklist has no effect on the model picker (confirmed for both built-in opencode and openrouter providers) #61110 — recent user report that
whitelisthas no effect on the model picker - Community workaround: GaboEI/opencode-model-filters-v2 (plugin via
ctx.model.transform)
Environment: opencode v2.2.5, macOS, shared background service. The "verified" points
above were tested empirically; happy to attach reproduction steps or help validate an
implementation.
Activity
opencodejr commentedon Nov 4, 2026
Thanks for the thorough write-up and for linking the earlier discussion. You're asking for per-model governance in the policy system, either by letting
provider.useresources matchproviderID/modelIDor by adding a dedicatedmodel.useaction. That would let admins enforce a fail-closed model allowlist with the same precedence and "only ever tighten" guarantees that policies already give providers, MCP servers, skills and plugins. Maintainers will consider it.I checked the current code, and it matches what you describe. The policy actions don't include a model-level one, and
provider.useis matched against the provider ID only, so this would need a core change rather than a plugin.Related issues:
model.use/ model allowlisting request, closed by the stale botwhitelist/blacklist(config-level)models["*"]disableddefault (config-level, complementary)whitelisthaving no effect on the model pickerprovider.usepolicy enforcement reportsIn the meantime, besides your virtual-provider workaround, the community plugin GaboEI/opencode-model-filters-v2 filters models via
ctx.model.transform. It doesn't give you policy precedence, though.sleetdrop commentedon Nov 4, 2026
Evaluated the plugin (thanks for the pointer). One granularity note for anyone considering it for this use case:
src/index.jsloads its rules once at plugin setup, from the global config only —configPath()is hardcoded to~/.config/opencode/opencode.json(orOPENCODE_CONFIG); project-level config files are not read. So the filters are per-user/per-machine, not per-project, and rule changes need a reload/restart to take effect (they are not re-read on config changes).That rules it out for our scenario, where different projects need different approved model sets. It does confirm the
ctx.model.transformmechanism handles registry-level filtering with fail-closed whitelist semantics, which is useful evidence for both this issue and #49986.Minor footnote: since it reuses the legacy
provider.<id>.whitelistfields, every config load also emits theomitted unsupported legacy settingnormalization warning for them (the plugin reads the raw file itself; OpenCode ignores those fields).sleetdrop commentedon Nov 10, 2026
Follow-up with a reproduction: the
ctx.model.transform+editor.remove()pattern recommended above is asilent no-op on 2.2.5 — the transform fires and the editor reflects the removals, but the catalog and
--modelresolution are unaffected. Details and a minimal repro: #63100