Skip to content

fix(core): do not fall back to another provider for an unregistered default - #63861

Open
xhorizont wants to merge 2 commits into
anomalyco:devfrom
xhorizont:provider-fallback
Open

xhorizont wants to merge 2 commits into
anomalyco:devfrom
xhorizont:provider-fallback

Conversation

@xhorizont

@xhorizont xhorizont commented Nov 13, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #58909

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

When a provider entry in opencode.json fails the config schema, the provider is never registered, but model still names it. Model.default then returned the first available model, so the prompt and its tool calls went to the built-in free provider without telling the user.

Now a configured default whose provider is not registered resolves to nothing, and ModelResolver.fallback no longer picks another model for it. The session fails with Model unavailable: probe/probe-1, the same message --model probe/probe-1 gives, so the user can see which entry to fix.

This applies to every configured provider that is not registered, not only schema-rejected entries: providers removed by disabled_providers/enabled_providers or a provider.use deny, and an unknown or misspelled provider ID, also stop instead of falling back. That is intended: in each case the user named a model, and silently sending the prompt elsewhere is the problem in #58909. Registered providers that are not active (activation: "disabled", or no connection) keep the existing fallback (covered by ignores a configured default on a disabled provider).

Model.configured() is a new method on the internal Core Model service; the plugin API and HTTP routes are unchanged. Model.default (behind the model.default route) can now return nothing in these cases, which its type already allowed.

How did you verify your code works?

  • New test in packages/core/test/model-free.test.ts covering Model.default, ModelResolver.fallback/resolve and the runner error. It fails on dev, and fails if either the resolver or the runner change is reverted.
  • Full packages/core suite passes (7135 pass, 0 fail) and bun typecheck passes.
  • I ran the config from the issue with isolated XDG_* dirs and no network. On dev: > build · big-pickle, then retries against opencode.ai. With this change: Error: Model unavailable: probe/probe-1.

Screenshots / recordings

N/A

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Added by the opencode bot (AI-generated).

Related: #55150

…efault

When `model` names a provider that was never registered, for example
because its config entry failed the schema, Model.default returned the
first available model, so prompts and tool calls went to the built-in
free provider. Resolve no default in that case. Registered providers that
are disabled or unavailable keep the existing fallback.

Fixes anomalyco#58909

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes the reported case. On the base, the config from #58909 ran on opencode/big-pickle and sent the request to the OpenCode endpoint. With this change the run stops with No model is available for session … and nothing is sent. The core model, catalog, resolver, session-runner and config tests pass, and tsc -b is clean.

Public API: no signatures change. Model.default (behind the plugin host and the server's model.default route) can now return nothing when the configured provider isn't registered. The type already allowed that.

One behaviour change is worth deciding on. The check is "is the provider in the provider records?", and providers removed by disabled_providers, enabled_providers or a provider.use policy are dropped from the records too. So model: anthropic/… together with disabled_providers: ["anthropic"] now hard-fails, where it used to fall back (I confirmed both). A typo or stale provider ID in model hard-fails the same way. That may be the intended safe behaviour, but it contradicts the description ("disabled providers keep the fallback" is only true for activation: "disabled"). And because the error names neither the session's model nor the configured provider/model, users hitting it won't know what to fix. Please either say that this is intended, or limit the check to providers that were dropped for invalid config. Including the configured model in the error would make either choice much easier to act on. See the inline comment.

Related: #55150

if (!requested) return value.available.find(supportsText)
// A provider that was never registered (for example, its config entry failed the schema) resolves to nothing,
// so prompts never go to a provider the user did not choose. Registered but unavailable providers still fall back.
if (!(yield* providers.snapshot()).records.has(requested.providerID)) return undefined

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

records also loses providers removed by disabled_providers, enabled_providers and provider.use policies (config/plugin/policy.ts calls providers.remove). It isn't only schema-rejected entries. With model: anthropic/claude-sonnet-4-5 and disabled_providers: ["anthropic"], the base falls back to opencode/big-pickle, and this change stops with No model is available for session …. A typo in model behaves the same way. Refusing to run on another provider may well be the right call here, but it's broader than the description says, and the error doesn't mention the configured provider/model, so users have nothing to go on. Can you confirm this is intended (and ideally name the configured model in the error)? Otherwise, limit the check to providers dropped for invalid config.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intended. In each of those cases the user named a model, and sending the prompt to another provider is exactly what #58909 is about, so stopping is the safer result. The description was wrong about this and is now fixed, along with the code comment. The error now names the configured model, Model unavailable: probe/probe-1, the same as --model, so it is clear which entry to fix (0df1aeb).

When no model resolves and config names a default, fail with
`Model unavailable: <provider>/<model>`, as `--model` does, instead of
`No model is available for session …`, so the user knows which entry to
fix. Also document that providers removed by `disabled_providers` or a
`provider.use` deny, and unknown provider IDs, no longer fall back.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both points from my earlier review are resolved:

  • You've confirmed that stopping for providers removed by disabled_providers, enabled_providers or provider.use is intended, and the description and code comment now say so.
  • The session error now names the configured model. With no model configured, the old No model is available error still applies.

I re-ran the config from the issue at this commit. It now fails with Model unavailable: probe/probe-1 and never reaches another provider. The disabled_providers case fails with Model unavailable: anthropic/claude-sonnet-4-5. The core model, catalog, resolver, session-runner and config tests pass, and tsc -b is clean.

Public API: unchanged. Model.configured() is only added to the internal core Model service; the plugin API and HTTP routes are unchanged.

No further concerns.

Related: #55150

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

config: the host drops an invalid provider entry and falls back to a built-in provider

1 participant