Skip to content

plugin: ctx.model.transform editor.remove() is a silent no-op for the model catalog and model resolution (2.2.5) #63100

Closed as not planned

Description

@sleetdrop

Description

A plugin using ctx.model.transform + editor.remove() to restrict a provider's models has no effect in 2.2.5: removed models still appear in GET /api/model and can still be selected and called via --model. There are no errors — the transform fires and the editor reflects the removals, but nothing propagates to the catalog or to model resolution.

This is the exact pattern recommended as the interim workaround for #49986 (comment) and mentioned again in #61601, so anyone following it gets silently non-functional filtering.

Expected: removing a model via the transform hides it from the catalog and makes explicit selection fail with Model unavailable, consistent with providers.<id>.models.<model>.disabled: true.

Plugins

Only the reproducing plugin itself (see steps). No other plugins.

OpenCode version

2.2.5 (macOS, arm64, shared background service; installed via homebrew)

Steps to reproduce

Prerequisite: any provider with ≥2 models (a custom openai-compatible provider in the user config works; no credentials needed for the catalog part).

  1. In a scratch directory, save .opencode/plugins/model-filter.ts:

    import { Plugin } from "@opencode/plugin"
    
    const PROVIDER = "<provider-id>"   // e.g. a custom provider
    const KEEP = "<one-model-id-on-it>"
    
    export default Plugin.define({
      id: "model-filter",
      async setup(ctx) {
        await ctx.model.transform((editor) => {
          for (const model of editor.list(PROVIDER)) {
            if (model.id !== KEEP) editor.remove(PROVIDER, model.id)
          }
        })
      },
    })
  2. Wait ~2s for the hot reload, then from that directory:

    opencode api get "/api/model?location%5Bdirectory%5D=$(pwd)"
    # → all of the provider's models are still listed
    
    opencode run --model <PROVIDER>/<a-removed-model> "Reply with exactly: OK"
    # → still succeeds

Evidence that the transform does run

Instrumenting the plugin with a file probe (plugin console.warn output does not appear in the server log):

12:09:59.189 transform registered
12:09:59.194 transform fired, removed 0, editor now lists 0      ← first pass, state still empty
12:09:59.217 transform fired, removed 16, editor now lists 3     ← second pass: removals applied

After the second firing the editor lists 3 models, yet GET /api/model for the same location returns 19, and explicit --model selection of a removed model succeeds. ctx.location inside the plugin matches the queried directory, so this does not look like a location mismatch.

Notes

  • The two firings suggest the state rebuild pipeline re-runs transforms as documented, and the editor's own state converges correctly — the removals just never reach the registry that GET /api/model and model resolution read. From a quick source pass, ctx.model is built from host.model (packages/plugin/src/promise/adapter.ts), and State (packages/core/src/state.ts) re-runs registered transforms on rebuild, which matches the observed firings. I could not pin down where the two views diverge.
  • The community plugin GaboEI/opencode-model-filters-v2 is built on the same hook and reports it working on 2.0.x, so this may be a regression in the 2.0 → 2.2.5 window.

References

Activity

  1. opencodejr commented on Nov 10, 2026

    @opencodejr
    Contributor

    Thanks for the detailed report and the probe output, they made this quick to track down.

    I tested on 2.2.5 with a plugin like yours and found that editor.remove() works, but only for models that are not listed in your config:

    • Built-in catalog provider (opencode): removing every model except one worked, and GET /api/model returned only the model I kept.
    • Custom provider whose models are listed under providers.<id>.models in config: the transform fired and the editor showed only the kept model, as in your probe. /api/model still listed all of them.

    The second case is how config precedence is designed to work. OpenCode applies config to models in a model transform that is registered after plugin transforms, so config wins. That step can bring back or re-enable any model listed in config. Because your custom provider lists its models in config, that step creates them again after your plugin removes them. The same code has been there since 2.0.x (I checked 2.0.9, 2.0.26 and 2.1.0), so I don't think this is a regression in the 2.0 → 2.2.5 window.

    Ways to hide those models now:

    • Disable the model in your plugin instead of removing it:
      editor.update(PROVIDER, model.id, (m) => { m.enabled = false })
      With this change, only the kept model showed up in /api/model in my test.
    • Or set providers.<id>.models.<model>.disabled: true in config, or stop listing those models there.

    You're right that the remove() suggestion on #49986 and #61601 doesn't mention this limitation. If you'd like remove() from a plugin to win over models listed in config, that would be a change to the designed precedence, so it may be better filed as a feature request. Related: #54480 (external transforms run before the config transform), #49986, #61601.

  2. opencodejr commented on Nov 13, 2026

    @opencodejr
    Contributor

    I'm closing this for now because there haven't been any replies since my last comment. On 2.2.5, editor.remove() worked for catalog models. Models listed under providers.<id>.models in config came back because the config model transform runs after plugin transforms, so config takes precedence. To hide those models now, set enabled = false from the plugin, or set disabled: true in config. If you'd like a plugin's remove() to take precedence over config, a feature request would be the right place for that. #54480, #49986 and #61601 are related.

    If you still see this with enabled = false, or with models that aren't listed in your config, reply here and the issue will reopen.

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