Skip to content

config: v1 migration overwrites custom provider baseURL with the legacy "api" value #50286

Closed

Description

@WuJunkai2004

Summary

The V1→V2 config migration writes the legacy provider.<id>.api protocol value (e.g. "openai") into settings.baseURL, destroying the real endpoint from options.baseURL. Every request to such a provider then fails instantly with AI.Error. Because the hidden title agent silently falls back to the session's primary model, the bug is nearly invisible: session titles keep generating while nothing ever reaches the configured endpoint.

Environment

  • opencode version: 2.0.7
  • OS: Windows_NT 10.0.26100 (win32 x64)
  • Terminal: Windows Terminal (TERM=xterm-256color)
  • Shell: Git Bash (C:\Program Files\Git\usr\bin\bash.exe, configured via shell)
  • Install/channel: latest (npm)
  • Active plugins: two local plugins (shell-env-fix, codearts-provider) — unrelated to this bug

Reproduction

  1. Put a V1-style custom OpenAI-compatible provider in ~/.config/opencode/opencode.jsonc (this exact config worked on 1.18.5):
{
  "agent": { "title": { "model": "llama.cpp/MiniCPM-V4.6" } },
  "provider": {
    "llama.cpp": {
      "name": "llama.cpp",
      "api": "openai",
      "options": {
        "apiKey": "<redacted>",
        "baseURL": "http://<llama-swap-host>:4000/v1"
      },
      "models": {
        "MiniCPM-V4.6": { "name": "MiniCPM", "tool_call": true }
      }
    }
  }
}
  1. Restart the service and inspect the resolved provider:
$ opencode api get /api/provider/llama.cpp
{"data":{"id":"llama.cpp",...,"package":"",
 "settings":{"apiKey":"<redacted>","baseURL":"openai"}}}

settings.baseURL is the literal string "openai" — the real URL from options.baseURL is gone. The same happens for every V1 provider entry that sets api.

  1. Create a session whose primary model is a different provider and send a single message. /api/agent/title shows the title agent correctly resolves to llama.cpp/MiniCPM-V4.6, but no request ever arrives at the endpoint. Verified against a llama-swap proxy: the backend model stays unloaded while the title is generated ~3s later by the primary-model fallback. This reproduced 100% of the time between 2026-09-18 and 2026-09-21 (every title in that window was served by the fallback).

Root cause

packages/core/src/v1/config/migrate.ts (tag v2.0.7), migrateStandardProvider:

settings: info.api ? { ...options.settings, baseURL: info.api } : info.options ? options.settings : undefined,

The V1 api field is a protocol selector ("openai", "anthropic", ...), not a URL. The migration spreads the real options.settings (which carries options.baseURL) and then overwrites baseURL with the protocol name. The same pattern exists in migrateModel:

settings: info.provider?.api ? { ...settings, baseURL: info.provider.api } : settings,

On current dev the migration was rewritten (migrateProvider now builds api: { type: "aisdk", ... }), but it still computes const url = info.api ?? options.url, so the protocol value still takes precedence over the real URL whenever both are present.

Expected Behavior

settings.baseURL should be the endpoint from options.baseURL. The legacy api selector should map to a runtime package (e.g. @opencode/ai/providers/openai-compatible), not overwrite the endpoint URL.

Actual Behavior

settings.baseURL === "openai"; every request to the provider fails instantly (AI.Error). For the title agent the failure is silent because packages/core/src/session/title.ts swallows AI.Error/provider errors and retries with the session's primary model, so users see "titles work fine" while their custom endpoint is never contacted and its logs show zero traffic.

Additional Context

  • Workaround (verified): rewrite the provider in native V2 shape — providers.<id> with package: "@opencode/ai/providers/openai-compatible" and settings.baseURL. After the rewrite, GET /api/provider/llama.cpp shows the correct URL and the title request reaches the endpoint (llama-swap spawns the backend model and the title is generated by it).
  • Secondary observation: with the corrected baseURL, cold-start title requests (llama-swap still spawning the backend) work — the request is held during the model load and the title is generated once the backend is ready (typically ~14s in my tests). One run took ~70s before the title appeared, so cold endpoints can delay the hidden title agent considerably, but no permanent failure was observed.
  • Related but distinct from providers: custom openai-compatible providers hang or send to undefined/chat/completions #36834, which covers options.baseURL being ignored (request to undefined/chat/completions) when no api field is set. This issue is the api-field variant: the endpoint is replaced by the literal protocol name.
  • Search terms: v1 migration, provider, baseURL, api field, openai-compatible, title agent, silent fallback.

Activity

  1. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    Thanks for the detailed report. I can confirm this on 2.0.7, and it still happens on the latest dev (fa1260d).

    What I did:

    • Used a V1-style provider in ~/.config/opencode/opencode.jsonc with "api": "openai" and options.baseURL: "http://127.0.0.1:4000/v1".
    • Ran opencode api --standalone get /api/config.

    On both builds, the migrated config shows providers.llama.cpp.settings as {"apiKey":"…","baseURL":"openai"}, so the endpoint from options.baseURL is lost.

    The cause is the one you pointed to: migrateStandardProvider and migrateModel in packages/core/src/v1/config/migrate.ts let the legacy api value override options.baseURL. That code hasn't changed on dev.

    The script below reproduces it. Until it's fixed, your workaround of writing the provider in the native V2 shape (providers.<id> with package and settings.baseURL) avoids the migration. Related: #36834 (options.baseURL ignored when no api field is set).

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Repro for #50286: V1 provider `api` field overwrites options.baseURL during migration.
    # Usage: OC="opencode" ./repro.sh   (or OC="bun ... packages/cli/src/index.ts")
    set -u
    OC=${OC:-opencode}
    T=$(mktemp -d)
    export HOME=$T/home XDG_CONFIG_HOME=$T/home/.config XDG_DATA_HOME=$T/home/.local/share XDG_STATE_HOME=$T/home/.local/state XDG_CACHE_HOME=$T/home/.cache
    mkdir -p $XDG_CONFIG_HOME/opencode $T/proj
    cat > $XDG_CONFIG_HOME/opencode/opencode.jsonc <<'JSON'
    {
      "provider": {
        "llama.cpp": {
          "name": "llama.cpp",
          "api": "openai",
          "options": { "apiKey": "sk-test", "baseURL": "http://127.0.0.1:4000/v1" },
          "models": { "MiniCPM-V4.6": { "name": "MiniCPM", "tool_call": true } }
        }
      }
    }
    JSON
    cd $T/proj
    $OC --version
    echo "--- migrated config (look at providers.llama.cpp.settings.baseURL) ---"
    $OC api --standalone get /api/config
    echo
    echo "--- resolved provider ---"
    $OC api --standalone get /api/provider/llama.cpp
    echo

    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