Skip to content

Model variants supplied by plugins are clobbered by late config transform #59836

Description

@PranjalAgarwal01

Description

After upgrading to v2.2.0, models whose reasoning tiers are supplied by an earlier model transform (e.g. a spec-pack plugin) lose their custom tiers on any location that has not been opened before. Fresh locations show generic Default/Low/Medium/High instead of the plugin's verified tiers (e.g. none/low/medium/xhigh).

Root cause

In packages/core/src/config/plugin/provider.ts, the late config transform re-resolves model.variants with the default effort list ["low","medium","high"] whenever a configured model has neither config.variants nor config.capabilities.reasoning. This overwrites variants already populated by an earlier transform (plugin), because the guard only checks source?.base / repackaged, not whether the model already carries variants.

Steps to reproduce

  1. Configure a provider whose models get custom variants from a plugin transform (no variants/capabilities.reasoning in opencode.json).
  2. Open a directory that has never been opened before (no cached catalog).
  3. Observe the model picker shows Default/Low/Medium/High instead of the plugin's tiers.

OpenCode version

2.2.0

Operating System

Windows 11

Activity

  1. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    Thanks for the detailed report. I reproduced this on 2.2.0, and it still happens on the latest dev (fa1260d).

    Setup:

    • A project-local plugin uses ctx.model.transform to set variants none/low/medium/xhigh (and a marker family) on a config-defined model, myprov/my-model.
    • opencode.json lists that model with no variants and no capabilities.reasoning.

    Result: model.list shows the plugin's family, so the plugin transform ran, but the variants are the generic low/medium/high. The late transform in packages/core/src/config/plugin/provider.ts re-resolves variants for custom models even when an earlier transform has already set them. When I skipped that step for models that already have variants, the plugin's tiers were kept. Your PR #59835 takes that approach.

    One difference from your report: each of my runs used a brand-new data directory and project, so I couldn't check whether locations opened before behave differently.

    The script below reproduces it. Run it with opencode from PATH, or with OC_DEV=<checkout> to run from source.

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Repro for #59836: plugin-supplied model variants are replaced by low/medium/high.
    # Usage: ./repro.sh            (uses `opencode` on PATH)
    #        OC_DEV=/path/to/opencode-checkout ./repro.sh   (runs from source)
    set -euo pipefail
    P=$(mktemp -d /tmp/opencode/proj.XXXX); export HOME=$(mktemp -d /tmp/opencode/home.XXXX)
    mkdir -p "$P/.opencode/plugins"; cd "$P"; git init -q
    cat > .opencode/plugins/spec-pack.ts <<'TS'
    export default {
      id: "spec-pack",
      setup: async (ctx: any) => {
        await ctx.model.transform((models: any) => {
          models.update("myprov", "my-model", (model: any) => {
            model.family = "spec-pack-ran" // proves the plugin transform ran
            model.variants = [{ id: "none" }, { id: "low" }, { id: "medium" }, { id: "xhigh" }]
          })
        })
      },
    }
    TS
    cat > opencode.json <<'JSON'
    {
      "provider": {
        "myprov": {
          "name": "My Provider",
          "npm": "@ai-sdk/openai-compatible",
          "options": { "baseURL": "http://127.0.0.1:9/v1", "apiKey": "x" },
          "models": { "my-model": { "name": "My Model" } }
        }
      }
    }
    JSON
    if [ -n "${OC_DEV:-}" ]; then
      OC=(bun --conditions=browser --preload="$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js" "$OC_DEV/packages/cli/src/index.ts")
    else OC=(opencode); fi
    "${OC[@]}" --version
    export OPENCODE_SERVER_PASSWORD=repro-pass
    "${OC[@]}" serve --hostname 127.0.0.1 --port 47123 > serve.log 2>&1 &
    SRV=$!; trap 'kill $SRV 2>/dev/null' EXIT
    for i in $(seq 1 60); do grep -q "listening" serve.log && break; sleep 1; done
    "${OC[@]}" api --server http://127.0.0.1:47123 model.list > models.json
    python3 - <<'PY'
    import json
    for m in json.load(open("models.json"))["data"]:
        if m["providerID"] == "myprov":
            print("family:", m.get("family"), "| variants:", [v["id"] for v in m["variants"]])
            print("expected: ['none', 'low', 'medium', 'xhigh']")
    PY

    Run it: bash repro.sh

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions