Skip to content

Plugin aisdk "sdk" hooks never run when the provider package is not on npm #42788

Closed

Description

@abhishekbiyala

Description

A plugin that registers a provider whose package is an aisdk: name that does not exist on npm never gets its ctx.aisdk.hook("sdk", ...) callback invoked. Dispatch fails with:

SessionRunnerModel.UnsupportedPackageError: Unsupported package for repro/test-model: aisdk:@example/not-on-npm

The package name here is only used as a routing key for the plugin's own hook, so nothing should be fetched from npm.

This is the same pattern OpenCode uses internally: GithubCopilotPlugin sets model.package = "@ai-sdk/github-copilot", claims it in its own sdk hook, and supplies the SDK from a local module. @ai-sdk/github-copilot is not published on npm. That works only because ProviderPlugins is ordered ahead of DynamicProviderPlugin inside pre; a plugin loaded from config cannot get in front of it.

Cause: DynamicProviderPlugin is registered in PluginInternal.pre, which runs before plugins loaded from config, so its sdk hook is always ahead of any plugin's. For an unrecognised package it calls npm.add(evt.package) with Effect.orDie, and the resulting 404 defect aborts the whole runSDK hook chain — every hook registered after it is skipped, including the plugin's.

Built-in providers are unaffected because their own hooks set evt.sdk earlier in the list, so if (evt.sdk) return short-circuits before the npm call.

This is hard to diagnose because resolveCatalogModel maps any loadAISDK failure to UnsupportedPackageError and drops the cause. I found it by temporarily logging each hook index in AISDK.run:

run-hook enter { index: 21 } -> done
run-hook enter { index: 22 } -> dynamic provider hook -> (no done)
NpmInstallFailedError: 404 Not Found - GET https://registry.npmjs.org/@example%2fnot-on-npm

Plugins

Standalone repro below, no dependencies.

OpenCode version

0.0.0-next-17444

Steps to reproduce

  1. Create repro-plugin/package.json:
{ "name": "repro-plugin", "version": "1.0.0", "type": "module", "main": "index.js" }
  1. Create repro-plugin/index.js:
export default {
  id: "repro",
  setup: async (ctx) => {
    await ctx.catalog.transform((catalog) => {
      catalog.provider.update("repro", (provider) => {
        provider.name = "Repro"
        provider.package = "aisdk:@example/not-on-npm"
        provider.settings = { baseURL: "https://example.invalid", apiKey: "repro" }
      })
      catalog.model.update("repro", "test-model", (model) => {
        model.name = "Repro Test Model"
        model.capabilities = { tools: true, input: ["text"], output: ["text"] }
        model.limit = { context: 100000, output: 4096 }
      })
    })

    await ctx.aisdk.hook("sdk", (event) => {
      console.error("sdk hook ran", event.package) // never printed
    })
  },
}
  1. Add the directory to plugins in opencode.json.
  2. Start a session on repro/test-model and send any prompt.

Expected: the plugin's sdk hook runs and can supply an SDK.
Actual: the hook never runs and the session fails with UnsupportedPackageError.

Operating System

Debian GNU/Linux 13 (trixie), Linux 6.12.63

Activity

  1. nkoynov commented on Oct 5, 2026

    @nkoynov

    This still happens on 2.0.22, and the code is unchanged on v2 @ ef07075: DynamicProviderPlugin still runs in PluginInternal.pre, before plugins loaded from config. The same ordering causes a second problem. We pin a plugin as cursor-opencode-provider@0.8.0, and it registers its provider with package: "aisdk:cursor-opencode-provider". OpenCode then installs a separate cursor-opencode-provider@latest copy and runs the language model from that copy, so the pinned plugin isn't the code that answers requests: patching only the pinned copy changed nothing, and patching the @latest copy did. Because @latest is a mutable cache entry, a new release can replace the model code under a pinned plugin without anyone noticing. Our workaround is "providers": {"cursor": {"package": "aisdk:cursor-opencode-provider@0.8.0"}}. A real fix could resolve an aisdk: package that a plugin registers against that plugin's own installed copy, or run the dynamic loader after config plugins, as #42789 did.

  2. opencodejr commented on Oct 28, 2026

    @opencodejr

    Thanks for the detailed report and for tracking down the cause. I can reproduce this on 2.2.0 (latest release) and on the latest dev (ced73dc0f6). Your exact build, 0.0.0-next-17444, isn't on npm any more, so I couldn't test that one.

    The plugin API has changed since your build, and ctx.catalog no longer exists. So I adapted your plugin to ctx.provider.transform + editor.add(...). It still uses the aisdk:@example/not-on-npm package and registers a ctx.aisdk.hook("sdk", ...) that writes to a log file. Then I ran opencode run -m repro/test-model "hello":

    • The plugin loads and its setup finishes.
    • The sdk hook is never called.
    • The run fails with Cannot initialize repro/test-model: 404 Not Found - GET https://registry.npmjs.org/@example%2fnot-on-npm - Not found.

    One part has already improved: the error now includes the npm 404 instead of an UnsupportedPackageError with no cause. The ordering problem you describe is unchanged on dev. DynamicProviderPlugin still registers its sdk hook ahead of plugins loaded from config, and the failed npm.add still stops the remaining hooks.

    The script below reproduces it. Set OC_DEV=/path/to/checkout to run it against a source checkout. Related: your earlier PR #42789 proposed a fix for this; it was closed by automated cleanup without being merged.

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Repro for #42788: a plugin's ctx.aisdk.hook("sdk") never runs for an aisdk: package that is not on npm.
    # Usage: ./repro.sh            -> runs `opencode` from PATH
    #        OC_DEV=/path/to/repo ./repro.sh -> runs the CLI from a source checkout
    set -u
    W=$(mktemp -d); P=$W/proj; mkdir -p $P/repro-plugin
    export HOME=$W/home XDG_CONFIG_HOME=$W/home/c XDG_DATA_HOME=$W/home/d XDG_STATE_HOME=$W/home/s XDG_CACHE_HOME=$W/home/k
    mkdir -p $HOME
    HOOKLOG=$W/hook.log
    echo '{ "name": "repro-plugin", "version": "1.0.0", "type": "module", "main": "index.js" }' > $P/repro-plugin/package.json
    cat > $P/repro-plugin/index.js <<JS
    import { appendFileSync } from "node:fs"
    const log = (m) => appendFileSync("$HOOKLOG", m + "\n")
    export default {
      id: "repro",
      setup: async (ctx) => {
        await ctx.provider.transform((editor) => {
          editor.add({
            info: { id: "repro", name: "Repro", activation: "enabled", package: "aisdk:@example/not-on-npm",
                    settings: { baseURL: "https://example.invalid", apiKey: "repro" } },
            models: [{ id: "test-model", modelID: "test-model", providerID: "repro", name: "Repro Test Model",
                       capabilities: { tools: true, input: ["text"], output: ["text"] }, variants: [],
                       time: { released: 0 }, cost: [], status: "active", enabled: true,
                       limit: { context: 100000, output: 4096 } }],
          })
        })
        await ctx.aisdk.hook("sdk", (event) => { log("SDK HOOK RAN " + event.package) })
        log("setup done")
      },
    }
    JS
    echo '{ "plugins": ["./repro-plugin"] }' > $P/opencode.json
    cd $P
    export OPENCODE_PASSWORD=repro-pass
    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)
      "${OC[@]}" --version
      # From source, the standalone child process does not inherit the preload flags, so run the server separately.
      "${OC[@]}" serve --hostname 127.0.0.1 --port 47888 > $W/server.log 2>&1 &
      SRV=$!; trap 'kill $SRV 2>/dev/null' EXIT
      for i in $(seq 60); do curl -s -o /dev/null http://127.0.0.1:47888/ && break; sleep 1; done
      timeout 180 "${OC[@]}" run --server http://127.0.0.1:47888 -m repro/test-model "hello" 2>&1 | tail -3
    else
      OC=(opencode); "${OC[@]}" --version
      timeout 180 "${OC[@]}" run --standalone -m repro/test-model "hello" 2>&1 | tail -3
    fi
    echo "--- plugin log (expect 'SDK HOOK RAN aisdk:@example/not-on-npm' if fixed):"
    cat $HOOKLOG

    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions