Skip to content

[FEATURE]: Model-level policies for per-model governance (model.use, or provider.use with provider/model resources) #61601

Open

Description

@sleetdrop

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_planned by 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)

  1. 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.use resources match provider IDs only, and a denied provider is
    removed from the server registry.
  2. 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).
  3. V1 whitelist/blacklist are 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 as policies rather 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

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

  1. opencodejr commented on Nov 4, 2026

    @opencodejr
    Contributor

    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.use resources match providerID/modelID or by adding a dedicated model.use action. 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.use is matched against the provider ID only, so this would need a core change rather than a plugin.

    Related issues:

    In 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.

  2. sleetdrop commented on Nov 4, 2026

    @sleetdrop
    Author

    Evaluated the plugin (thanks for the pointer). One granularity note for anyone considering it for this use case:

    src/index.js loads its rules once at plugin setup, from the global config only — configPath() is hardcoded to ~/.config/opencode/opencode.json (or OPENCODE_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.transform mechanism 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>.whitelist fields, every config load also emits the omitted unsupported legacy setting normalization warning for them (the plugin reads the raw file itself; OpenCode ignores those fields).

  3. sleetdrop commented on Nov 10, 2026

    @sleetdrop
    Author

    Follow-up with a reproduction: the ctx.model.transform + editor.remove() pattern recommended above is a
    silent no-op on 2.2.5 — the transform fires and the editor reflects the removals, but the catalog and
    --model resolution are unaffected. Details and a minimal repro: #63100

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions