Repository navigation
[Bug]: OpenCode 2 custom providers / models omitted during boot probe race in makeOpenCode2ModelLoader #15155
Description
Activity
#14962 might be related to this
Note
Grok responding on behalf of Julius.
Triage
Thanks @hardik88t for the timing breakdown and for tracking down the OpenCode API note! I confirmed this on current
main(18b21325). Custom OpenCode providers get dropped because the startup catalog probe treats the first non-empty model list as complete.What happens
makeOpenCode2ModelLoaderinapps/server/src/provider/Layers/OpenCodeProvider.tspollsGET /api/modelevery 250ms and stops atuntil: (models) => models.length > 0, with a 5s cap. The comment above it only talks about waiting out an empty catalog. The tests cover "empty, then one model" and "keep the last list if a later read is empty", but not a list that keeps growing after the first models appear. OpenCode's list-models API says the snapshot may come before plugins settle, which matches the 29 built-inopencode-gomodels arriving before your custom providers.That partial list is what the picker keeps. The OpenCode driver (
apps/server/src/provider/Drivers/OpenCodeDriver.ts) probes once at startup.refreshOnIntervalis false, and changing settings doesn't re-check the provider. Opening a project only refreshes that directory's skills and commands. A ready probe counts as successful discovery, so the shorter list also replaces any fuller catalog already in the provider status cache.Settings → refresh runs the same loader again, but it isn't a reliable fix. The owned server stops after 30s idle, so the next check spawns a fresh
opencode serveand can stop on the built-ins again.Workarounds
Both of yours hold.
customModelsgets merged in after the probe, and an externalserverUrlthat has already settled returns the full list on the first read.I didn't find an existing issue for this boot-probe race. The stop condition hasn't changed since
8ed276c(0.0.46-nightly.20261003). A maintainer will decide on the fix direction.Reacted by Sebastián Maluk- addedbugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.via-triageFiled through npx t3 triageFiled through npx t3 triage
on Oct 3, 2026 Additional, distinct failure path for the same symptom (custom / plugin providers missing from the picker), found while debugging
commandcode+litellm:opencode models --verbosewas removed in OpenCode v2loadInventoryFromCliinapps/server/distbuilds the OpenCode inventory by shelling out to:const runModelsCli = () => runOpenCodeCommand({ args: ["models", "--verbose"], // ← flag no longer exists in OpenCode v2 ... })
and parses the result with
parseModelsCliOutput, which expects each model as a slug line followed by a JSON block (the old--verboseformat), derivingconnectedfrom providers whose JSON parsed. On OpenCode 2.0.22 that flag is rejected:$ opencode models --verbose ERROR Unrecognized flag: --verbose in command opencode models $ echo $? 1
So no JSON blocks are produced, the locally-registered providers never enter
connected, andflattenOpenCodeModelsfilters them out (if (!connected.has(provider.id)) continue).Evidence
OpenCode itself is healthy and exposes everything:
$ opencode models | cut -d/ -f1 | sort | uniq -c 75 commandcode/ 10 litellm/ 29 opencode-go/ 81 opencode/
GET /api/provider?directory=<dir> → [opencode-go, opencode, commandcode, litellm] GET /api/model?directory=<dir> → 195 models, 75 with providerID=commandcodeBut the picker shows 110 (= 81
opencode+ 29opencode-go) — i.e. exactly the 85 models from the locally-registered providers are dropped.Relationship to the boot-probe race in this issue
These look like two independent causes producing the same symptom:
- The boot-probe race you describe (first non-empty snapshot wins) — affects
/api/modeltiming. - This one — the CLI inventory path depends on a removed
--verboseflag, so plugin/config providers are dropped even when settlement is fine.
Note (2) also survives a full T3 restart, since it re-runs the same broken command every time — consistent with the report that a reboot doesn't help.
Suggested fix
Don't depend on the removed
--verboseflag. Prefer the HTTP API (/api/provider+/api/model), or use plainopencode modelsand resolve metadata elsewhere, gated by OpenCode version. Adding a fallback so a CLI-contract change degrades gracefully would also prevent silent provider loss.Environment
- T3 Code
0.0.46-nightly.20261003.2632, OpenCode2.0.22, orchestrator v2 - Backend in WSL (
wslOnly: true),archlinux - Providers:
commandcode(OpenCode v2 plugin registered via~/.config/opencode/opencode.json→"plugins": ["./plugin/commandcode"]),litellm(inline inopencode.json)
- The boot-probe race you describe (first non-empty snapshot wins) — affects
Confirming the race on macOS with plain inline config providers (no plugins), against a managed server:
- T3 Code
0.0.46-nightly.20261003.2638, OpenCode2.0.22, noserverUrl. ~/.config/opencode/opencode.jsoncdefines two custom providers: one@ai-sdk/openai-compatible(75 models) and one@ai-sdk/anthropic(9 models).- The managed server's
GET /api/modelreturns 123 models (75 + 9 + 29opencode-go+ 10opencode). - T3's live provider catalog holds 114 models and none from the
@ai-sdk/anthropicprovider, i.e. exactly the snapshot where the first provider had settled and the second had not. - The server has been up for 12+ minutes and reports all 123, but the driver's
refreshOnInterval: falsemeans the picker keeps the partial list.
So this also hits inline config providers, and the stale snapshot outlives the race window on a long-lived settled server.
- T3 Code
Independent reproduction confirming the root cause, with a case where the first non-empty read is not even close to complete.
I hit this on OpenCode 2.0.22 with T3 Code nightly (
0.0.46-nightly.20261003.2638and0.0.46-nightly.20261004.2644) across several machines, and the symptom was exactly the picker showing the 29 built-inopencode-gomodels while a custom OpenAI-compatible provider stayed missing even though the OpenCode TUI listed it fine.The first non-empty read can be a tiny fraction of the final catalog
Polling a freshly-spawned
opencode serveevery 300 ms (same env T3 uses):0ms 0 {} 600ms 29 {'opencode-go': 29} 900ms 43 {'opencode-go': 29, 'opencode': 10, '<custom>': 4} 1200ms+ 43 {…}That machine's final catalog was only 43, so "stop at 29" loses one custom provider. But on another machine the same race is much worse — OpenCode exposes more providers (cloud built-ins plus the custom one), and the catalog kept growing well past the 5 s cap:
0ms 0 6s 0 8s 264 ← first non-empty read: built-ins only ... ... settled 207+ across 8 providerIDs, and still climbingSo
until: (models) => models.length > 0is not just "off by one provider" — the first non-empty read can be a small early slice, and the 5 stimeoutOptiondoesn't help because polling has already stopped. The reporter's ~450 ms → ~900 ms timeline is one instance of a general "first non-empty ≠ settled" problem.Direct evidence from T3's own trace
The trace shows T3 making the exact call and reading the partial value:
GET http://127.0.0.1:<port>/api/model?location[directory]=/<home> -> 200 len=60 (empty) GET http://127.0.0.1:<port>/api/model?location[directory]=/<home> -> 200 len=2096 (~1 s later, still the built-in-only set)and
checkOpenCode2exitsSuccesswith that shorter list, which then replaces the provider snapshot.A second shape of the same bug: persisted empty list
Because the loader only caches a
> 0read (lastLoadedstarts[]and is only written whenloaded.value.length > 0), a server whose catalog is still empty at the 5 s timeout leaveslastLoadedempty, and the provider probe can end up with zero models. I saw this as"OpenCode 2 is running, but it did not list any models yet."even thoughopencode api model.listagainst the same server returned a full catalog seconds later. Worth covering in the fix: "never got a non-empty read in the window" should be an error/retry state, not a persisted empty snapshot.Workarounds that both held for me (matches the triage note)
providerInstances.opencode.config.customModels— merged in after the probe.- External
serverUrlpointed at an already-warmopencode serve— returns the full catalog on the first read. This is what I standardized on: a persistentopencode serveon a fixed port (systemd user unit on Linux, LaunchAgent on macOS) plusserverUrl/serverPassword, which makes the race disappear entirely.
Environment
- opencode: 2.0.22 (curl v2 install,
~/.opencode/bin/opencode) - T3 Code: 0.0.46-nightly.20261003.2638 and 0.0.46-nightly.20261004.2644
- OS: Linux (Rocky 8.10, Ubuntu 26.04), macOS 26
- Reproducible consistently on the default managed server; disappears with a warm external
serverUrl.
I reproduced a functional variant of this startup race: discovery caches providers that enabled_providers later disables, which blocks working models and advertises unusable ones.
Recorded install: T3 Code 0.0.46-nightly.20261003.2638, OpenCode 2.0.22, macOS. Local verification: macOS 26.5.2 arm64; upstream main 4ee6bfd, #15202 diff at 2a963b6, #15510 diff at e555f5f.
Repro with a managed server (no serverUrl):
- Connect OpenCode Console/OpenRouter, configure Ollama, and set enabled_providers to ["opencode-go"]. Use fresh discovery state.
- Confirm warm
opencode modelslists only 29 Go models. On the reported install, CLI turns using opencode-go/kimi-k3 and opencode-go/glm-5.3 both returnedsubagent-ok. - Delegate to opencode-go/kimi-k3 in T3: rejected before child creation with "Model opencode-go/kimi-k3 is not advertised by provider opencode."
- Delegate to T3's advertised opencode/kimi-k3: a child starts, then fails with "Model unavailable: opencode/kimi-k3". The reported install showed the same pair of failures for GLM 5.3; restarting T3's OpenCode servers did not repair its catalog.
I independently reproduced both Kimi failures on upstream main in an isolated headless backend harness. Its actual loader accepted 473 models (opencode 81, openrouter 390, ollama 2), with zero Go models. With either PR's diff, discovery returned the correct 29 and a T3-owned delegated Kimi task completed with
subagent-ok. This exercises the real delegation service, orchestrator and OpenCode 2 adapter; the parent and provider-registry facade are test fixtures, and no UI was tested. Only credentials/config were copied into disposable XDG directories; no live sessions were copied.A separate 50 ms catalog trace measured: 0 at 179 ms → 471 at 342 ms → 473 at 410 ms → 502 at 692 ms → 29 at 761 ms. integration.list completed at 738 ms; the directory's model.updated arrived at 757 ms. Reads immediately after either returned 29. Both PRs also retained three custom models with a controlled 800 ms plugin-activation delay, which main omitted.
Another way this shows up: delegating to an OpenCode plugin model from a Cursor parent. I reproduced it on main @ e22c880 with OpenCode 2.0.22 and
cursor-opencode-provider@0.8.0, using a managed server in a fresh HOME.- After start, the provider says
OpenCode 2.0.22 lists 10 models.(the built-inopencode/*only). The plugin's 59cursor/*models show up only after a manual refresh, which then lists 69. - In that state,
delegate_taskwith target{providerInstanceId: "opencode", model: "cursor/claude-opus-5-5-1m"}is rejected inresolveTarget("not advertised"). But the Cursor parent never sees that reason. Because of [Bug]: Typed MCP tool failures return isError:false and violate the advertised success schema #15566 (fix in fix(mcp): mark declared tool failures as errors #15617), it gets:MCP error -32602: Structured content does not match the tool's output schema: data must have required property 'taskId', data must have required property 'childThreadId', … 'waitTimedOut' - Does fix(opencode): wait for provider activation before caching models #15202 fix this? I timed a fresh
opencode servewith this plugin:- When the plugin is already in OpenCode's npm cache,
integration.listreturns at 0.72–0.82 s and all 69 models are already listed. fix(opencode): wait for provider activation before caching models #15202 fixes that case. - On the first start after the plugin is installed or updated,
integration.listreturns at 4.66 s with only the 10 built-ins, and the 59 plugin models appear at 5.86 s, past the 5 s budget. fix(opencode): wait for provider activation before caching models #15202 would still keep 10 there, so a later re-probe is still needed.
- When the plugin is already in OpenCode's npm cache,
Model: Claude Opus 5.5 (1M). Harness: Claude Code in T3 Code.
- After start, the provider says
- added a commit that references this issue
on Oct 6, 2026 I'm having a similar problem in
makeOpenCode2ModelLoader, from a different cause, with one extra twist that (perhaps) matters for the fix.My setup: macOS, T3 Code Nightly, OpenCode
2.0.20. The only enabled provider isgithub-copilot(Copilot Enterprise), and my organisation limits which models are allowed. A corporate proxy blocksmodels.opencode.ai:level=ERROR message="Failed to fetch models.dev" cause="... 403 GET https://models.opencode.ai/api.json"So
~/.cache/opencode/models.jsonhasn't been updated in months. Once OpenCode is fully running, it gets the correct list from the Copilot Enterprise API, which only includes my organisation's allowed models. Running OpenCode directly shows that list. While the server is still starting,/api/modelanswers from the outdated saved file instead.What T3 saves (
~/.t3/caches/opencode.json):message: OpenCode 2.0.20 lists 35 models. contains github-copilot/gpt-6.1-sol: false ← allowed and working contains github-copilot/gpt-6-astra: true ← no longer offered, fails with "Model unavailable"The same managed server, asked a few seconds later:
GET /api/model?directory=<project> → 12 models, includes gpt-6.1-solHow this differs from the original report: here the first answer has more models than the final one (35 → 12), and some of them don't work. So a fix like "keep polling until the model count stops growing" wouldn't help in my case. The early list has to be replaced, not added to. T3's merge step (
mergeProviderModelskeeping previous models) could also keep the dead models around.What doesn't help: restarting T3 and deleting
~/.t3/caches/opencode.json. Every startup runs into the same timing and saves the outdated list again.What would fix both cases:
- After the server starts, check the model list again: on OpenCode's provider/plugin-settled event if it has one, otherwise after a short delay. Replace the saved list with the result instead of merging it.
- Make sure the refresh/re-check action in provider settings asks the running server again instead of reusing T3's saved list.
Workaround for now: add the missing slug by hand under
customModels.
Before submitting
Area
apps/server
Steps to reproduce
~/.config/opencode/opencode.jsonc), for example:{ "$schema": "https://opencode.ai/config.json", "provider": { "proxy": { "npm": "@opencode/ai/providers/openai-compatible", "name": "Proxy Gateway", "options": { "baseURL": "http://127.0.0.1:8317/v1", "apiKey": "..." }, "models": { "claude-sonnet-4-6": { "name": "Claude Sonnet 4.6" }, "gpt-6-luna": { "name": "GPT 6 Luna" } } } } }opencode modelsin the terminal to verify the custom models are listed and active.serverUrlconfigured).Expected behavior
All models configured in OpenCode (including custom providers and plugins) are discovered and listed in T3 Code's model picker and provider settings without requiring manual entry into
customModels.Actual behavior
Only the 29 built-in
opencode-gomodels are discovered. Custom providers and proxy models defined in OpenCode config are missing.Root Cause Analysis
In
apps/server/src/provider/Layers/OpenCodeProvider.ts(insidemakeOpenCode2ModelLoader):In OpenCode v2, provider plugins initialize asynchronously:
/api/modelexplicitly documents:opencode serve,/api/modelreturns the 29 built-inopencode-gomodels within ~450ms.models.length > 0evaluates totrue(29 > 0),makeOpenCode2ModelLoaderimmediately terminates polling on the first retry.Impact
Major degradation or frequent failure
Version or commit
0.0.46-nightly.20261003.2610 (commit 8ed276c on main)
Environment
Linux x86_64, T3 Code Desktop 0.0.46-nightly, OpenCode 2.0.22
Logs or stack traces
Workaround
Either:
settings.jsonunderproviderInstances.opencode.config.customModels.opencode serveindependently and configure T3 Code to connect viaserverUrl, bypassing the boot probe race.