Skip to content

Setting provider.<id>.options.baseURL changes which model opencode picks by default #47815

Closed as not planned

Description

@zmanian

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:

  1. Do not let a provider.<id> block that supplies no credential and no models
    affect default model selection — treat options.baseURL as configuration
    for a provider rather than an election of it.
  2. Rank a provider the user has a credential for above one that only has
    options set.
  3. 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

  1. seven7763 commented on Sep 7, 2026

    @seven7763

    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): a provider.<id> block from config is merged onto the matching database[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 when provider.integrationID === undefined && !integration. A config-sourced block with no env/auth and no plugin integration satisfies exactly that, so all those inherited models enter model.available().

    Why the default flips to that specific model: catalog.ts:215-232 (model.default()) has no persisted default, so it sorts model.available() by time.released descending and takes the head. Claude Sonnet's release timestamp beats gpt-5.3-chat-latest's, so adding the block elects the newest Anthropic catalog entry — and only then does the missing ANTHROPIC_API_KEY surface 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-738 and 774-775 (cloudflare-workers-ai / cloudflare-ai-gateway) return { autoload: false } precisely when options.baseURL is 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.

  2. thdxr commented on Oct 11, 2026

    @thdxr
    Member

    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 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions