Skip to content

OpenCode 2: first opencode run fails with "Model unavailable" when the provider is under providers and the model cache is cold #46

Closed

Description

@zyc

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-litellm 1.7.1
  • OpenCode 2.0.32 and 2.1.0 (same result on both)
  • LiteLLM proxy, 6 chat models, /v1/models and /v1/model/info reachable
  • 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

  1. Empty $XDG_CACHE_HOME/opencode-litellm/.
  2. opencode run --standalone -m litellm/<any model> "hi" (stdin closed).
  3. Error above. Repeat: same error.
  4. 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:

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 during setup, 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 with includeModels / excludeModels is 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

  1. zyc commented on Oct 20, 2026

    @zyc
    ContributorAuthor

    Update: I reported the host-side ordering to OpenCode (anomalyco/opencode#57421). Their answer confirms the analysis above:

    • It works as designed: all plugin setups run in one batch, and the config-declared providers.* are added by an internal plugin in that same batch, so ctx.provider.list() cannot see them while setup runs.
    • Plugins cannot read the config from ctx either, so a plugin cannot get providers.litellm.settings during setup by another route.
    • Discovery that starts after setup is not something opencode run waits for.
    • The route they point to for cold one-shot runs is exactly the plugin entry options ("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 run work, 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 providers array inside the plugin options (each entry merged over the shared options, discovered during setup)? 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 temperature for models that list it) is open, in case it is useful context.

  2. zyc commented on Oct 20, 2026

    @zyc
    ContributorAuthor

    Opened #48 (draft) with this: a providers list 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 name is 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.

  3. zyc commented on Oct 20, 2026

    @zyc
    ContributorAuthor

    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-provider package.

    The new version takes providers in the plugin options keyed by id and written the way the config's providers key 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.

  4. zyc commented on Oct 21, 2026

    @zyc
    ContributorAuthor

    Update. We are no longer proposing the providers map from #48 and are closing it. Our users now run with the config exactly as the README documents (plugins plus providers.*), 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 providers config, real LiteLLM proxy):

    • In an interactive session (TUI, web, desktop) or opencode serve, discovery finishes and the models appear.
    • A one-shot opencode run through the background service fails the first time and works a few seconds later.
    • A one-shot opencode run --standalone fails, 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.

  5. yuseferi commented on Oct 26, 2026

    @yuseferi
    Owner

    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.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions