Summary
After PR #25167 (commit 560baae, merged April 30 2026, shipped in v1.14.x), the plugin provider.models() hook can no longer populate models for custom providers — i.e. providers declared by the user in opencode.jsonc whose id is not present in the public models.dev catalog. This is a regression: the same plugin code worked on versions shipped before April 30.
Root cause (located in packages/opencode/src/provider/provider.ts)
The PR moved the plugin hook iteration from running after config processing to before, and switched the lookup from the live providers map to the pre-config database map:
Before (worked):
// ran AFTER configProviders were merged into providers
for (const hook of plugins) {
...
const provider = providers[providerID] // live map, includes user-config providers
if (!provider) continue
provider.models = await models(provider, ...)
}
After PR #25167 (broken for custom providers):
// runs BEFORE configProviders loop
for (const hook of plugins) {
...
const provider = database[providerID] // models.dev catalog only
if (!provider) continue // ← skips any custom provider
provider.models = await models(provider, ...)
}
database is built from modelsDev (the public models.dev catalog at ~/.cache/opencode/models.json). Any provider declared exclusively in user config — e.g. a local proxy, a self-hosted endpoint, or any other non-catalog provider — is not in database, so if (!provider) continue short-circuits the hook entirely.
The PR's stated intent ("ensure user config takes precedence over plugin hooks") is reasonable, but the implementation also removed the ability for plugins to register models for non-catalog providers.
Reproduction
-
Declare a custom provider in opencode.jsonc not present in models.dev (e.g. an OpenAI-compatible local proxy):
-
Write a plugin that exposes the v2 provider.models() hook:
export default {
id: \"my-custom-proxy\",
server: async () => ({
provider: {
id: \"my-custom-proxy\",
models: async () => ({
\"some-model\": { id: \"some-model\", providerID: \"my-custom-proxy\", api: { id: \"some-model\", url: \"http://localhost:9090/v1\", npm: \"@ai-sdk/openai-compatible\" }, name: \"Some Model\", capabilities: { /* ... */ }, cost: { input: 0, output: 0, cache: { read: 0, write: 0 } }, limit: { context: 200000, output: 64000 }, status: \"active\", options: {}, headers: {} }
}),
},
}),
};
-
Expected: The provider appears with the model the hook returned.
-
Actual on v1.14.x: The provider is registered (because the static config block exists), but the plugin hook is skipped entirely. No models from the hook appear; only models defined statically in opencode.jsonc show up.
Adding the provider's id to the static models.dev catalog isn't user-facing, so users with self-hosted/proxy providers cannot rely on dynamic model discovery via plugins anymore.
Impact
This breaks any plugin that augments a non-catalog provider with dynamically discovered models — e.g. local LLM proxies that expose /v1/models, internal corporate gateways, OpenAI-compatible self-hosted servers. Users now have to hardcode every model in opencode.jsonc and update it manually whenever the upstream catalog changes.
Suggested fix
Move the plugin hook iteration back to run after configProviders are merged into providers, and look up providers[providerID] instead of database[providerID]. The original ordering supported both catalog and custom providers. To preserve PR #25167's intent (user config taking precedence over plugin hooks), apply config models on top of plugin-supplied models in a final reconciliation pass, rather than by reordering the hooks.
Environment
Summary
After PR #25167 (commit
560baae, merged April 30 2026, shipped in v1.14.x), the pluginprovider.models()hook can no longer populate models for custom providers — i.e. providers declared by the user inopencode.jsoncwhoseidis not present in the public models.dev catalog. This is a regression: the same plugin code worked on versions shipped before April 30.Root cause (located in
packages/opencode/src/provider/provider.ts)The PR moved the plugin hook iteration from running after config processing to before, and switched the lookup from the live
providersmap to the pre-configdatabasemap:Before (worked):
After PR #25167 (broken for custom providers):
databaseis built frommodelsDev(the public models.dev catalog at~/.cache/opencode/models.json). Any provider declared exclusively in user config — e.g. a local proxy, a self-hosted endpoint, or any other non-catalog provider — is not indatabase, soif (!provider) continueshort-circuits the hook entirely.The PR's stated intent ("ensure user config takes precedence over plugin hooks") is reasonable, but the implementation also removed the ability for plugins to register models for non-catalog providers.
Reproduction
Declare a custom provider in
opencode.jsoncnot present in models.dev (e.g. an OpenAI-compatible local proxy):{ \"provider\": { \"my-custom-proxy\": { \"npm\": \"@ai-sdk/openai-compatible\", \"name\": \"My Custom Proxy\", \"options\": { \"baseURL\": \"http://localhost:9090/v1\", \"apiKey\": \"unused\" } } } }Write a plugin that exposes the v2
provider.models()hook:Expected: The provider appears with the model the hook returned.
Actual on v1.14.x: The provider is registered (because the static config block exists), but the plugin hook is skipped entirely. No models from the hook appear; only models defined statically in
opencode.jsoncshow up.Adding the provider's id to the static models.dev catalog isn't user-facing, so users with self-hosted/proxy providers cannot rely on dynamic model discovery via plugins anymore.
Impact
This breaks any plugin that augments a non-catalog provider with dynamically discovered models — e.g. local LLM proxies that expose
/v1/models, internal corporate gateways, OpenAI-compatible self-hosted servers. Users now have to hardcode every model inopencode.jsoncand update it manually whenever the upstream catalog changes.Suggested fix
Move the plugin hook iteration back to run after
configProvidersare merged intoproviders, and look upproviders[providerID]instead ofdatabase[providerID]. The original ordering supported both catalog and custom providers. To preserve PR #25167's intent (user config taking precedence over plugin hooks), apply config models on top of plugin-supplied models in a final reconciliation pass, rather than by reordering the hooks.Environment
v1.14.33@opencode-ai/[email protected]