Repository navigation
Description
Summary
Adding a provider.<id> block to opencode.json — even one that only sets
options.baseURL, with no credential and no model named — promotes that provider
into the configured set and changes which model opencode picks by default. For a
user who has no credential for that provider, a working opencode stops working.
This is surprising because setting a base URL reads as "if I use this provider,
send it here", not "use this provider".
Reproduction
opencode 1.18.27, macOS (arm64). Two runs, each with a fresh HOME and a
scratch OPENCODE_CONFIG, differing only in the config file. In both,
OPENAI_API_KEY is set and there is no Anthropic credential anywhere.
A — baseline
{ "$schema": "https://opencode.ai/config.json" }$ opencode run "say hi"
> build · gpt-5.3-chat-latest
Error: Incorrect API key provided: probe-openai. ...
Model selection is correct: it picks an OpenAI model, and the request reaches
OpenAI. (The key is deliberately invalid; the point is where the call went.)
B — one base URL added
{
"$schema": "https://opencode.ai/config.json",
"provider": { "anthropic": { "options": { "baseURL": "http://127.0.0.1:8899/anthropic/v1" } } }
}$ opencode run "say hi"
> build · claude-sonnet-4-6
Error: Anthropic API key is missing. Pass it using the 'apiKey' parameter or the ANTHROPIC_API_KEY environment variable.
The default model flipped to an Anthropic one and the run fails. The flip
happens before any network call, so it is not the loopback URL being
unreachable — it is the block existing.
Not specific to Anthropic
The same pair with the built-in nearai provider, again with OPENAI_API_KEY
only and no NEARAI_API_KEY:
| config | model chosen |
|---|---|
| empty | gpt-5.3-chat-latest |
provider.nearai.options.baseURL set |
google/gemini-3-pro |
So it is the presence of any provider.<id> block that reorders selection, not
something about a particular provider.
Also affects a provider the user does use
Worth stating for completeness, because this half works exactly as documented:
with an Anthropic credential present, adding the same baseURL correctly
redirects. A probe server on the configured base logs the call arriving:
HIT POST /anthropic/v1/messages host=127.0.0.1:8899
So the redirect behaviour is right. It is only the coupling to provider
selection that is surprising.
Why it matters beyond a surprising default
Anything that configures opencode on a user's behalf — a proxy's setup command,
a corporate config template, a devcontainer — has no safe way to set a base URL.
Writing the key is correct for a user of that provider and breaks a user who is
not, and the two are indistinguishable from outside.
Suggested directions
Any one of these would resolve it; I have no attachment to which:
- Do not let a
provider.<id>block that supplies no credential and no models
affect default model selection — treatoptions.baseURLas configuration
for a provider rather than an election of it. - Rank a provider the user has a credential for above one that only has
options set. - If the coupling is intentional, documenting it on the
provider.options
field would be enough — the schema description ("Custom provider
configurations and model overrides") does not currently hint at it.
Happy to send a PR if you can say which direction you would take.
Activity
seven7763 commentedon Sep 7, 2026
Reproduced the election flip and traced it to the catalog's availability predicate. The mechanism is a two-step chain, and it's narrower than "any config block promotes a provider":
Step 1 — the config block inherits the upstream model catalog.
provider.ts:1423-1434(mergeProvider): aprovider.<id>block from config is merged onto the matchingdatabase[providerID]entry (the models.dev-style catalog), so{"anthropic": {"options": {"baseURL": ...}}}arrives with anthropic's entire stock model list attached — the block didn't have to declare any models to get them.Step 2 — a credential-less, integration-less provider counts as "available".
catalog.ts:71-77:available()returns true whenprovider.integrationID === undefined && !integration. A config-sourced block with noenv/auth and no plugin integration satisfies exactly that, so all those inherited models entermodel.available().Why the default flips to that specific model:
catalog.ts:215-232(model.default()) has no persisted default, so it sortsmodel.available()bytime.releaseddescending and takes the head. Claude Sonnet's release timestamp beatsgpt-5.3-chat-latest's, so adding the block elects the newest Anthropic catalog entry — and only then does the missingANTHROPIC_API_KEYsurface at call time.On the suggested directions: option 1 (options-only blocks don't elect) treats the symptom at the catalog layer, but note the repo already has a working precedent for "baseURL means configure, not elect" — the custom loaders at
provider.ts:736-738and774-775(cloudflare-workers-ai / cloudflare-ai-gateway) return{ autoload: false }precisely whenoptions.baseURLis set, i.e. "URL configured → don't treat this as activation". Extending that same semantic to the generic config-block path would be consistent with existing behavior. Option 2 (credential-ranked) would also change behavior for integration-based providers that legitimately have no static credential, so it's the broader blast radius of the two.One more data point for whichever direction is picked: the failure is silent at the config layer — the user only sees the wrong-provider error at call time. If the coupling is kept intentionally (option 3), it would be worth a startup-level warning when a provider becomes "available" with zero credential sources (env, config env, auth, integration all empty), because that state is currently indistinguishable from a working setup until the first request fails.
thdxr commentedon Oct 11, 2026
Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (
opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.— from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊