Repository navigation
Description
Summary
After the package is updated, opencode can keep loading an old version of opencode-cmd-provider even though npm reports the latest version as installed. The reason is that the same package exists in two cache directories at the same time and only one of them is refreshed.
Environment
- OS: Ubuntu 24.04.4 LTS
- opencode: 1.18.30
- opencode-cmd-provider:
@latestdir at 1.7.3, bare dir at 1.6.2 - npm: reports 1.7.3 as the latest installed version
What happens
The package lives in two cache directories simultaneously:
~/.cache/opencode/packages/opencode-cmd-provider@latest # loaded as the plugin
~/.cache/opencode/packages/opencode-cmd-provider # loaded as the runtime provider (model.api.npm)
This is a consequence of how opencode resolves the two roles:
-
the plugin loader appends
@latest(<name>-><name>@latest), so the plugin lives inopencode-cmd-provider@latest; -
the provider/SDK loader uses
model.api.npmverbatim, and this plugin registers that as the bare name:// src/plugin/index.ts autoRegister(config, MODEL_SNAPSHOT, { npm: "opencode-cmd-provider", // no version ... })
Because opencode keys its cache by the exact specifier string, opencode-cmd-provider@latest and opencode-cmd-provider are two separate directories. When the package is updated, only the @latest copy is refreshed. The bare copy stays at the previous version, and opencode keeps loading that stale copy at runtime (its Npm.add returns an existing directory as-is).
So the effective state is: npm says the latest version is installed, but opencode still runs the old one.
Reproduction
- Install the plugin so both dirs exist at some version
X. - Publish/release a newer version
Y. - Update the package so that only the
@latestdir moves toY. - Check the runtime: opencode still uses the bare dir at
X, whilenpmreportsYas installed.
Why this is a plugin-side issue
The plugin registers its runtime provider by an unpinned bare name. That guarantees a second cache entry which cannot follow the plugin version and can silently go stale. Pinning the provider specifier to the plugin's own version would make the cache entry version-addressed, so an update would necessarily fetch the matching provider code and the two would never diverge.
Proposed fix
Register the runtime provider with a version-pinned specifier instead of the bare name, e.g.:
npm: `opencode-cmd-provider@${PLUGIN_VERSION}`so the plugin and the provider it loads always come from the same release.
Related
Upstream opencode-side root cause (cache keyed by exact specifier + no refresh of existing installs): anomalyco/opencode#48514
Activity
rashidrazak commentedon Sep 12, 2026
Investigation: plugin defect or upstream defect?
Short answer: the root cause is upstream. The cache design that makes divergence
possible is opencode's, and it is not new. This plugin owns the trigger (it registers an
unpinned provider specifier) and it is the only side that can neutralize it today, but
pinning is a mitigation, not a fix — and it does not fix the half that actually hurts
users: installs that never move forward. The chosen fix and the trade-offs we weighed are
at the end of this comment.
Verified mechanism (opencode 1.18.30: source at tag
v1.18.30+ the shipped binary)packages/core/src/npm.ts:79→const directory = (pkg) => path.join(global.cache, "packages", sanitize(pkg)).packages/core/src/npm.ts:125→if (yield* afs.existsSafe(path.join(dir, "node_modules", name))) return resolveEntryPoint(...). Still unchanged ondev, so no upstream fix has landed.packages/opencode/src/plugin/shared.ts:210appends@latestfor a bare plugin spec;packages/opencode/src/provider/provider.ts:1846callsNpm.add(model.api.npm)with the provider's value verbatim.Confirmed in the installed 1.18.30 binary:
npmfor a package that is the plugin guarantees a second cache key that is never version-tied to the plugin. Note the plugin has no API that tells it which specifier loaded it, so it cannot choose "the same key" — it can only choose a specifier that cannot go stale independently.Registering a bare provider
npmis upstream's documented convention (docs use"npm": "@ai-sdk/openai-compatible"; built-ins usegitlab-ai-provider), sosrc/plugin/index.ts:14is not violating a contract. The contract simply has no case for"provider is this same package".
Live reproduction (isolated
HOME, opencode 1.18.30, this machine)Seeded
packages/opencode-cmd-provider@latestwith 1.7.1 (byte-for-byte this machine'sreal cache), project config
"plugin": ["opencode-cmd-provider"], no declared provider:opencode modelscommandcode/*models auto-registered → the 1.7.1 plugin ranopencode run --model commandcode/claude-sonnet-5Npm.add("opencode-cmd-provider")created the bare dir at 1.7.4 (npmlatest)…@latest= 1.7.1, bare = 1.7.4That is the divergence reproduced end-to-end, in the reverse direction of the report
(runtime newer than the plugin registering its models), because a bare spec resolves to
npm
latestwhenever that dir is missing. The run leg then hit the known upstream hang(zero requests reached the mock), which does not affect this finding.
This machine's own cache also shows the other half:
opencode-cmd-provider@^1.6.4and…@latestboth sit at 1.7.1 while npmlatestis 1.7.4 — both copies are stale purelyfrom the no-refresh behavior, at 91 MB each.
Attribution
rekram1-node. Upstream's own text on #48514: "A plugin author can work around this by pinning a version, but opencode should not depend on that discipline to keep a plugin internally consistent."rm -rf ~/.cache/opencode/packages/opencode-cmd-provider*step stays mandatory.Decision: pin the provider to the plugin's version
We're going with the fix proposed in the report: pin the provider specifier to the
plugin's own version, exact —
opencode-cmd-provider@<version>, read from thepackage.jsonthat ships next todist. Not a range: a range would just be anothercache key that can drift on its own.
We also looked at a
file://self-reference (npm: new URL("../../index.js", import.meta.url).href), where opencode imports the plugin's own entry file and skipsNpm.addcompletely. It removes the second install, but it leans on an internal branchof the loader, bypasses the normal npm path (mirrors, proxies, integrity), and would fail
hard if that branch ever changes. For a package people install the normal way, that's the
wrong risk to take. Pin it is.
Downside vs upside of the pin
package.jsonmust match the code indistpackage.json; if that breaks, the plugin dies or the bug returnsprovider.commandcode.npmthemselves doesn't get the fixNet: the upsides are about being correct and predictable; the downsides are about space
and patience, plus one dev-only trap. Silent and confusing is worse than slow and obvious.
What changes for users, exactly
Worth being precise, because it's easy to read this fix backwards: users never got
automatic plugin updates, and they still won't. The only "free" update today is the
provider half installing newest when its folder happens not to exist — which is exactly
how this issue's mismatch is born, in either direction. The fix removes that path and
leaves the documented one: delete the cache folder, restart.
What this does not fix
The stale-install half is upstream's: a cache keyed by the exact specifier string, and
Npm.addnever refreshing an install that already exists. That isanomalyco/opencode#48514 (now assigned), and it stays the real fix — every opencode
version in the wild still needs the manual cache wipe, so the README and CHANGELOG will
keep saying so explicitly.
Implementation
package.jsonnamedopencode-cmd-provider; if that can't be read, fall back totoday's bare name so the plugin never fails to load.
package.json's version.docs/adr/(the auto-registration mechanism lives in ADR-0001).rashidrazak commentedon Sep 12, 2026
Spec published: #151 — Spec: version-pinned provider specifier — plugin and runtime provider never diverge (labelled
ready-for-agent).It carries the chosen fix (exact version pin read from the shipped
package.json, with a bare-name fallback so the plugin never fails to load), the two seams the behaviour will be tested at, the accepted costs, and the explicit statement that updating still requires clearing the package cache.rashidrazak commentedon Sep 12, 2026
Shipped on
mainvia PR #153 (merge1c183c0) — not yet released; npm still serves 1.7.4, so no user can install it yet.What changed: the runtime provider is now registered as
opencode-cmd-provider@<exact version of the running plugin>, so opencode's specifier-keyed cache can no longer hold a runtime provider from a different release than the plugin that registered it — in either direction. Verified with the isolated-HOMEprobe from this report against real opencode 1.18.30: before, the cache held…@latest= 1.7.1 next to a bare directory at 1.7.4; now there is exactly one entry, and it matches the plugin.What did not change: the stale-install half.
Npm.addstill returns an existing directory without checking its version, so an already-cached plugin stays at the version it was installed at until the package cache is cleared —rm -rf ~/.cache/opencode/packages/opencode-cmd-provider*remains the update path, and ADR-0009 says so explicitly rather than letting this fix read as "updates now happen automatically". That half is upstream's: anomalyco/opencode#48514.Keeping this open until a release carrying the fix is published, so it is not closed against a version users cannot install.
rashidrazak commentedon Sep 12, 2026
Released as 1.7.5 — GitHub Release · npm
[email protected](latest), published 2026-09-12 with provenance attestations.From this release on, the runtime provider is registered under the plugin's exact version (
opencode-cmd-provider@<version>), so opencode's specifier-keyed package cache can no longer hold a runtime provider from a different release than the plugin that registered it. Verified against real opencode 1.18.30 with the isolated-HOMEprobe from this report: before, the cache held…@latest= 1.7.1 alongside a bare directory at 1.7.4; now there is exactly one entry, and it matches the plugin.To pick it up:
rm -rf ~/.cache/opencode/packages/opencode-cmd-provider*, then restart OpenCode. The cache wipe is still required —Npm.addnever refreshes an existing install, so neither half moves on its own. That half is unchanged by design and remains upstream: anomalyco/opencode#48514.Closing as fixed for the divergence described here; the staleness behaviour is tracked upstream.