Skip to content

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

Description

@zmanian

Description

Confirmed on OpenCode v2.0.26, Linux x86_64, installed from @opencode/cli@2.0.26. Reopening per the V2 triage comment: the provider-selection coupling still reproduces with both native V2 configuration and the original V1 configuration syntax.

Each run used a separate, initially empty HOME and project directory, env -i, only OPENAI_API_KEY=probe-openai, and run --standalone so no existing background service or credentials could affect selection. The key is deliberately invalid; no real provider credentials were used. No explicit model was selected.

Observed results

Configuration Selected provider/model Result
Empty {} opencode/exo-free HTTP 410: model deprecated
Only native V2 Anthropic settings.baseURL anthropic/claude-haiku-5-5 Connection refused at the configured loopback URL
Baseline with the built-in opencode provider excluded openai/gpt-6.1-sol HTTP 401: invalid probe OpenAI key
Same exclusion + only Anthropic settings.baseURL anthropic/claude-haiku-5-5 Connection refused at the configured loopback URL
Original V1 provider.anthropic.options.baseURL syntax, without the exclusion anthropic/claude-haiku-5-5 Connection refused at the configured loopback URL

The built-in free model now wins the empty-config baseline, so I also ran the controlled pair excluding that provider identically in both configs. This restores the original OpenAI → uncredentialed Anthropic comparison without choosing a model explicitly. Model IDs were checked in the persisted assistant messages, not inferred from errors.

Controlled V2 reproduction

Install @opencode/cli@2.0.26 and confirm opencode --version prints opencode v2.0.26. Create a.json:

{
  "experimental": {
    "policies": [
      { "action": "provider.use", "resource": "opencode", "effect": "deny" }
    ]
  }
}

Create b.json, differing only by the base-URL overlay:

{
  "experimental": {
    "policies": [
      { "action": "provider.use", "resource": "opencode", "effect": "deny" }
    ]
  },
  "providers": {
    "anthropic": {
      "settings": { "baseURL": "http://127.0.0.1:8899/anthropic/v1" }
    }
  }
}

Run each with an isolated profile (nothing needs to listen on port 8899):

bin="$(command -v opencode)" # absolute path to the 2.0.26 CLI
configs="$PWD"
scratch="$(mktemp -d)"
for case in a b; do
  mkdir -p "$scratch/$case/home" "$scratch/$case/project"
  (
    cd "$scratch/$case/project"
    env -i PATH="$PATH" HOME="$scratch/$case/home" \
      OPENAI_API_KEY=probe-openai \
      OPENCODE_CONFIG="$configs/$case.json" \
      "$bin" run --standalone 'say hi'
  )
done

A reaches OpenAI and reports Incorrect API key provided: probe-openai. B prints build · claude-haiku-5-5, retries, and reports ConnectionRefused.

On Linux, selection can also be checked independently in the local session database:

SELECT json_extract(data, '$.model.providerID') AS provider,
       json_extract(data, '$.model.id') AS model
FROM session_message
WHERE type = 'assistant';

Run this against each $scratch/<case>/home/.local/share/opencode/opencode.db: A records openai|gpt-6.1-sol; B records anthropic|claude-haiku-5-5.

V2 implementation path

The current V2 source still couples an overlay to activation:

The exact model names and terminal error have changed since V1: V2 attempts the configured URL rather than raising the original missing-Anthropic-key error first. The reproduced issue is still that an endpoint-only overlay changes provider/model selection without any credential or model declaration. The expectation remains: configuring where a provider would send requests should not, by itself, elect that provider as the default.

Plugins

No response

OpenCode version

No response

Steps to reproduce

No response

Screenshot and/or share link

No response

Operating System

No response

Terminal

No response

Activity

  1. opencode-agent commented on Oct 12, 2026

    @opencode-agent
    Contributor

    Thanks for the very precise report. I reproduced this with your two configs and the same isolated setup (env -i, fresh HOME and project dir, only OPENAI_API_KEY=probe-openai, no model selected).

    • 2.0.26: config A selects openai|gpt-6.1-sol and fails with the invalid-key 401. Config B, which only adds providers.anthropic.settings.baseURL, selects anthropic|claude-haiku-5-5 and fails with ConnectionRefused. Both results come from the persisted assistant messages.
    • Latest dev (4d9678a): same result.

    The script below runs both cases and prints the persisted provider/model for each.

    As you point out, the config provider transform marks every configured provider as enabled, even a settings-only one, and enabled providers count as available without a credential. Related: #47815 (the same report against v1) and #42790 (credential-less configured providers).

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Reproduces #54686: an endpoint-only provider overlay changes the implicit default model.
    # Usage: ./repro.sh            (uses `opencode` from PATH)
    #        OC_DEV=/path/to/repo ./repro.sh   (runs from source checkout)
    set -u
    here="$(cd "$(dirname "$0")" && pwd)"
    cat > "$here/a.json" <<'J'
    { "experimental": { "policies": [ { "action": "provider.use", "resource": "opencode", "effect": "deny" } ] } }
    J
    cat > "$here/b.json" <<'J'
    { "experimental": { "policies": [ { "action": "provider.use", "resource": "opencode", "effect": "deny" } ] },
      "providers": { "anthropic": { "settings": { "baseURL": "http://127.0.0.1:8899/anthropic/v1" } } } }
    J
    if [ -n "${OC_DEV:-}" ]; then
      run=(bun --conditions=browser --preload="$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js" "$OC_DEV/packages/cli/src/index.ts")
    else
      run=("$(command -v opencode)")
    fi
    "${run[@]}" --version
    scratch="$(mktemp -d)"
    for c in a b; do
      mkdir -p "$scratch/$c/home" "$scratch/$c/project"
      echo "=== case $c ==="
      envs=(env -i PATH="$PATH" HOME="$scratch/$c/home" OPENAI_API_KEY=probe-openai OPENCODE_CONFIG="$here/$c.json")
      if [ -n "${OC_DEV:-}" ]; then
        # From source, --standalone cannot spawn its child server (it drops the bun preload),
        # so start an equally isolated server ourselves and attach to it.
        port=$((20000 + RANDOM % 20000))
        ( cd "$scratch/$c/project" && OPENCODE_PASSWORD=probe "${envs[@]}" OPENCODE_PASSWORD=probe "${run[@]}" serve --port "$port" >"$scratch/$c/serve.log" 2>&1 ) &
        spid=$!
        for _ in $(seq 60); do curl -s -o /dev/null "http://127.0.0.1:$port" && break; sleep 1; done
        ( cd "$scratch/$c/project" && "${envs[@]}" OPENCODE_PASSWORD=probe timeout 90 "${run[@]}" run --server "http://127.0.0.1:$port" 'say hi' 2>&1 | uniq | tail -n 8 )
        pkill -P "$spid" 2>/dev/null; kill "$spid" 2>/dev/null; sleep 1
      else
        ( cd "$scratch/$c/project" && "${envs[@]}" timeout 90 "${run[@]}" run --standalone 'say hi' 2>&1 | uniq | tail -n 8 )
      fi
      db="$(find "$scratch/$c/home" -name "opencode*.db" | head -1)"
      echo "--- persisted assistant model ($db):"
      [ -n "$db" ] && DB="$db" bun -e 'import {Database} from "bun:sqlite"; const d=new Database(process.env.DB,{readonly:true}); console.log([...new Set(d.query("SELECT json_extract(data,\"$.model.providerID\")||\"|\"||json_extract(data,\"$.model.id\") AS m FROM session_message WHERE type=\"assistant\"").all().map(r=>r.m))].join("\n"))'
    done

    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