Repository navigation
OpenCode 2: first opencode run fails with "Model unavailable" when the provider is under providers and the model cache is cold #46
Description
What happened?
On OpenCode 2, with the LiteLLM provider declared under providers (the "Minimal config (recommended)" form) and no model cache yet (first run, cache cleared, expired, or a changed config/credential), a one-shot opencode run fails:
Error: Model unavailable: litellm/<model>
Running it again fails the same way, because the process exits before the background discovery writes the cache. Once a long-lived process (TUI or opencode serve) has populated ~/.cache/opencode-litellm/, opencode run works.
Environment
opencode-plugin-litellm1.7.1- OpenCode 2.0.32 and 2.1.0 (same result on both)
- LiteLLM proxy, 6 chat models,
/v1/modelsand/v1/model/inforeachable - Node 22, macOS
Config
{
"plugins": ["opencode-plugin-litellm@1.7.1"],
"providers": {
"litellm": {
"package": "@opencode/ai/providers/openai-compatible",
"settings": { "baseURL": "http://localhost:4000/v1", "apiKey": "{env:LITELLM_API_KEY}" }
}
}
}Steps to reproduce
- Empty
$XDG_CACHE_HOME/opencode-litellm/. opencode run --standalone -m litellm/<any model> "hi"(stdin closed).- Error above. Repeat: same error.
- Start
opencode serve(or the TUI) for a few seconds, stop it, repeat step 2: works.
Analysis
I instrumented listMatchingProviders and polled context.provider.list() for 3.5 s right after setup started: it returned zero LiteLLM providers the whole time. The host registers providers.* only after setup has returned, so:
setupcannot see the configured provider, and- waiting inside
setupfor it cannot work (I prototyped a bounded wait; it only delays startup, and the host's ~5 s activation limit makes it risky), and - the reconcile on
provider.updated(OpenCode 2: configured LiteLLM provider is never seen — provider.list() is read once during setup, before OpenCode registers providers #34/fix: discover LiteLLM providers registered after plugin setup #35) does find the provider and publishes it, but a one-shot command has already resolved the model by then.
So the existing reconcile is correct for long-lived sessions; this is specifically the cold + one-shot case (CI, scripts, the "test your setup" command in guides).
What works today
- A warm cache (see above).
- Plugin options instead of
providers("plugins": [{ "package": "...", "options": { "baseURL": ..., "apiKey": ... } }]): discovery runs duringsetup, so the first run works. Limitation: one provider per plugin entry. Declaring the same package twice only keeps the last entry (the first returns "Model unavailable"), so splitting one proxy into several providers withincludeModels/excludeModelsis not possible this way.
Possible direction
Allow several providers in the plugin options, all discovered during setup, e.g.:
"plugins": [{
"package": "opencode-plugin-litellm@1.7.1",
"options": {
"baseURL": "http://localhost:4000/v1",
"apiKey": "{env:LITELLM_API_KEY}",
"providers": [
{ "providerID": "litellm", "excludeModels": ["prod-*"] },
{ "providerID": "litellm-exp", "includeModels": ["prod-*"] }
]
}
}]A rough prototype (about ten lines in setup: build one source per entry with makeProviderSource(context, undefined, { ...pluginOptions, ...entry })) made a cold-cache opencode run succeed for both providers, and includeModels / excludeModels kept the split (a model excluded from one provider is unavailable there). I have not run the test suite on it. Happy to turn it into a PR with tests if this direction is acceptable, or to try another approach you prefer.
I am also filing the host-side ordering (providers registered after plugin setup) with OpenCode, since a fix there would make this unnecessary.
Activity
zyc commentedon Oct 20, 2026
Update: I reported the host-side ordering to OpenCode (anomalyco/opencode#57421). Their answer confirms the analysis above:
setups run in one batch, and the config-declaredproviders.*are added by an internal plugin in that same batch, soctx.provider.list()cannot see them whilesetupruns.ctxeither, so a plugin cannot getproviders.litellm.settingsduringsetupby another route.setupis not somethingopencode runwaits for."plugins": [{ "package": ..., "options": { ... } }]). They filed the readiness/ordering side as a feature request and linked fix(core): fresh standalone run rejects a model before catalog refresh completes anomalyco/opencode#56119 (a readiness latch for the models.dev catalog, close to the "one-shot run waits" idea).So until the host changes, plugin options are the supported way to make a cold
opencode runwork, and the remaining gap on the plugin side is the one described here: a single plugin entry can only describe one provider, so splitting one proxy into several providers (includeModels/excludeModels) is not possible cold.Given that, would you accept the
providersarray inside the plugin options (each entry merged over the shared options, discovered duringsetup)? If yes I will turn the prototype into a PR with tests. If you would rather wait for the host-side fix, that is fine too; the options form keeps working for a single provider in the meantime.Unrelated to this issue: #47 (declare
temperaturefor models that list it) is open, in case it is useful context.zyc commentedon Oct 20, 2026
Opened #48 (draft) with this: a
providerslist in the plugin options, each entry merged over the shared options and discovered during setup, concurrently.Two things beyond the first prototype. Reconcile now keeps each entry's own filters and label: the host reflects the plugin's own publications back as LiteLLM-shaped providers, and rebuilding them from the shared options leaked every model into every provider. And
nameis read from the options, so the providers get their own labels instead of all showing up as "LiteLLM (proxy)".Happy to change the option shape if you prefer another one.
zyc commentedon Oct 20, 2026
I reworked #48 after thinking about the option shape. The first version was a list with a
providerID, each entry merged over the shared options. It worked, but it meant inheritance rules (an entry that only changed the address would still get the shared key), a repeated id resolving silently to the last one, and no way to set a per-providerpackage.The new version takes
providersin the plugin options keyed by id and written the way the config'sproviderskey is (name,package,settings,headers), so a block can be moved between the two places as is. Each provider is self-contained and discovered during setup, concurrently. It is about half the code of the first version, and 1.7.1 behavior is unchanged without the key: I ran 14 scenarios against the published 1.7.1 and this branch and compared the registry, every request and the logs, and they are identical (details in the PR).Still a draft. Happy to change the shape if you prefer another one.
zyc commentedon Oct 21, 2026
Update. We are no longer proposing the
providersmap from #48 and are closing it. Our users now run with the config exactly as the README documents (pluginsplusproviders.*), so no new option surface is needed on your side.The cold one-shot limitation stays as the OpenCode team described it in anomalyco/opencode#57421: the host registers config providers after plugin setup and does not wait for discovery. What we measured on OpenCode 2.0.32 and 2.1.0 (empty plugin cache, README
providersconfig, real LiteLLM proxy):opencode serve, discovery finishes and the models appear.opencode runthrough the background service fails the first time and works a few seconds later.opencode run --standalonefails, and since the host ends the process about 5 s after the failure, the discovery often does not reach the disk cache either. We prototyped waiting for the discovery on dispose, but it only helps when the gateway answers very fast (cache complete only up to roughly 0.25 s of added latency per request), so we are not proposing it.For anyone hitting it: opening an interactive session once warms the cache. Thanks for the review time.
yuseferi commentedon Oct 26, 2026
Thanks for the thorough investigation, and for taking it upstream. I agree with the conclusion. OpenCode registers config providers only after plugin setup returns, and one-shot commands don't wait for discovery, so the plugin can't fix this on its own. Waiting inside setup runs into the host's activation limit, and waiting on dispose only helps with very fast gateways. Neither is worth shipping.
I've documented the limitation and both workarounds (warm the cache once, or use plugin options for CI and scripts) in a README FAQ entry: #51. That PR closes this issue when it merges. If OpenCode ships the readiness latch (anomalyco/opencode#56119), we can revisit and make cold one-shot runs work with the
providers.*config too.