Skip to content

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

Open

Description

@iceteaSA

What occurs

The host drops a provider entry in opencode.json when the entry does not agree with the configuration schema. The host writes one warning to the service log. The host then continues. The configured model names a provider that the catalog does not contain.

Model selection then uses the first model of the catalog that is available. That model is a free model of the built-in OpenCode provider. The prompt and all tool calls go to that provider. The client does not report that the host dropped the configured provider.

An explicit model selection on the command line stops with an error for the same configuration. Only the configured model falls back to a different provider.

Steps to reproduce

The reproduction is possible without network access. Run the host in a network namespace so that no request goes out of the namespace.

unshare -rn
ip link set lo up

Use isolated configuration, data, state, and cache directories so that the active user configuration is not read.

export XDG_CONFIG_HOME="$SCRATCH/config"
export XDG_DATA_HOME="$SCRATCH/data"
export XDG_STATE_HOME="$SCRATCH/state"
export XDG_CACHE_HOME="$SCRATCH/cache"
export HOME="$SCRATCH/home"

Write $XDG_CONFIG_HOME/opencode/opencode.json:

{
  "update": "disable",
  "model": "probe/probe-1",
  "providers": {
    "probe": {
      "name": "Probe",
      "package": "aisdk:@ai-sdk/openai-compatible",
      "settings": {
        "apiKey": "test-key",
        "baseURL": "http://127.0.0.1:9/v1"
      },
      "models": {
        "probe-1": {
          "name": "Probe 1",
          "capabilities": {
            "tools": true,
            "reasoning": true,
            "input": ["text"],
            "output": ["text"]
          }
        }
      }
    }
  }
}

The field capabilities.reasoning is invalid. The V2 schema accepts only an object, for example {"supported": true}.

Run one prompt:

opencode run --standalone "hi"

The service log contains one warning:

level=WARN message="configuration normalization diagnostic" path=$.providers.probe kind=invalid action="skipped malformed recognized value"

The client output does not name the provider probe:

> build · big-pickle

The session database records the provider and the model of the assistant message as opencode and big-pickle.

The host then sends a request to the endpoint of the built-in provider. The request does not succeed in the namespace. The run ends with an error that names no provider:

Error: ECONNREFUSED: Unable to connect. Is the computer able to access the url?

With network access, the same request succeeds and the model answers.

Correct result

The run stops with an error that the client shows. The error names the provider probe. The host does not use a provider that the user did not select.

A warning alone is sufficient only if the client shows the warning. The client must not start a model request after the host drops a provider entry.

Actual

The host drops the invalid provider entry. The host continues with a different provider. The prompt and the tool calls go to the built-in OpenCode provider. The client shows the model name in the run header. The client does not report that the host rejected the configured provider. Only the service log contains the configuration warning.

The same configuration with an explicit model selection stops with an error:

opencode run --standalone --model probe/probe-1 "hi"
Error: Model unavailable: probe/probe-1

Why this is important

A configuration mistake sends the prompt and the tool access to a provider that the user did not select. A user who configures a private, local, or self-hosted provider can send source code, secrets in the prompt, and tool results to a hosted provider. The user receives no notice of that change.

The failure is also hard to find. The client output names the model that ran. The client output does not state that the host dropped the configured provider. The user must read the service log to find the cause.

Workarounds

Two settings stop the fallback. The two runs below use the configuration above.

The setting disabled_providers removes the built-in provider from the catalog. The run then stops with an error.

{
  "disabled_providers": ["opencode"]
}
Error: No model is available for session <session id>

The environment setting OPENCODE_DISABLE_FREE_MODELS=1 removes the free models of the built-in provider. The run then stops with the same error.

Source locations

The line numbers refer to commit 17b79f4e0c16dcff200b0417567ed22aeb2d7166 on the dev branch.

  • packages/core/src/config/normalize.ts:169 reads the providers map and decodes each entry.
  • packages/core/src/config/normalize.ts:699-717 drops an entry when the decode does not succeed. The other entries continue.
  • packages/core/src/config/normalize.ts:842-844 records the diagnostic with the type invalid and the message skipped malformed recognized value.
  • packages/core/src/config.ts:122-130 logs each diagnostic as a warning. The other configuration values continue.
  • packages/schema/src/config/provider.ts:73-78 specifies the Reasoning field as an object.
  • packages/schema/src/config/provider.ts:98-104 adds that field to the model configuration.
  • packages/core/src/config/plugin/provider.ts:108 reads the configured model.
  • packages/core/src/config/plugin/provider.ts:115-116 stores it as the default model. The code does not check that the catalog contains the provider.
  • packages/core/src/model.ts:271-276 selects the requested model only when the catalog contains the model and the host enables the model. In the other condition the method selects the first available model that accepts text.
  • packages/core/src/model.ts:296-312 puts the free model big-pickle first in the list of free models. As a result the first available model is opencode/big-pickle.
  • packages/core/src/plugin/provider/opencode.ts:368-372 enables the built-in provider without a credential. The code sets the API key to public when the host has no key and no connection.
  • packages/core/src/session/runner/model.ts:77-89 selects the model of the agent when the catalog contains the model. As a result the model resolver runs in the other condition.
  • packages/core/src/model-resolver.ts:416-431 calls the default model. The code then calls the first available model.

Version

The reproduction uses v2.1.4. The same code is in dev at the commit above. At 32b57015e5b491a789fba1534ecb472e676c9d8e these files have only import changes.

Activity

  1. opencode-agent commented on Oct 25, 2026

    @opencode-agent
    Contributor

    Thanks for the very precise report, @iceteaSA. I reproduced this on v2.1.4, and it still happens on the latest dev (32b5701).

    With your config (top-level model: probe/probe-1, plus capabilities.reasoning: true in the probe model):

    • opencode run --standalone "hi" writes the configuration normalization diagnostic ... path=$.providers.probe kind=invalid warning to the service log only. The run header shows > build · big-pickle, and the session stores the assistant message as opencode / big-pickle. In my sandbox, with network access, the hosted model actually answered. On dev, the request still went to the OpenCode endpoint and was refused only because of the free-usage limit.
    • opencode run --standalone --model probe/probe-1 "hi" fails with Error: Model unavailable: probe/probe-1, as you described.

    So a configured default model whose provider was dropped falls back to the first available model, which is the built-in OpenCode provider, and the client shows nothing about it. The script below reproduces it.

    Related issues: #50340 and #53744 (one invalid field drops the whole provider, with only a log warning), #54786 (rejected config files are ignored without a visible error) and #55150 (work unexpectedly going to a built-in free model).

    Until this is addressed, "disabled_providers": ["opencode"] or OPENCODE_DISABLE_FREE_MODELS=1 prevents the fallback, as you noted.

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Reproduce #58909: invalid provider entry is dropped; configured model falls back to opencode/big-pickle.
    # Usage: OPENCODE="opencode" bash repro.sh   (or set OPENCODE to a source-run command)
    set -u
    OPENCODE=${OPENCODE:-opencode}
    S=$(mktemp -d /tmp/opencode/oc58909.XXXX)
    export XDG_CONFIG_HOME="$S/config" XDG_DATA_HOME="$S/data" XDG_STATE_HOME="$S/state" XDG_CACHE_HOME="$S/cache" HOME="$S/home"
    mkdir -p "$XDG_CONFIG_HOME/opencode" "$HOME" "$S/proj"
    cat > "$XDG_CONFIG_HOME/opencode/opencode.json" <<'JSON'
    {
      "update": "disable",
      "model": "probe/probe-1",
      "providers": {
        "probe": {
          "name": "Probe",
          "package": "aisdk:@ai-sdk/openai-compatible",
          "settings": { "apiKey": "test-key", "baseURL": "http://127.0.0.1:9/v1" },
          "models": {
            "probe-1": {
              "name": "Probe 1",
              "capabilities": { "tools": true, "reasoning": true, "input": ["text"], "output": ["text"] }
            }
          }
        }
      }
    }
    JSON
    cd "${PROJ:-$S/proj}"
    echo "=== run with configured model (no --model) ==="
    timeout 60 $OPENCODE run --standalone --print-logs "hi" 2>&1 | grep -Ei 'diagnostic|big-pickle|probe|build ·|Error|provider' | head -30
    echo "=== run with --model probe/probe-1 ==="
    timeout 60 $OPENCODE run --standalone --model probe/probe-1 "hi" 2>&1 | tail -5
    echo "=== assistant messages in DB ==="
    DB=$(find "$XDG_DATA_HOME" -name '*.db' | head -1); echo "db: $DB"
    bun -e 'const {Database}=require("bun:sqlite");const d=new Database(process.argv[1]);for(const r of d.query("select type,data from session_message").all()){const j=JSON.parse(r.data);if(r.type==="assistant")console.log("assistant model:",JSON.stringify(j.model),"text:",JSON.stringify(j.content?.[0]?.text));if(j.error)console.log(r.type,JSON.stringify(j.error))}' "$DB"

    Run it: bash repro.sh

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions