Repository navigation
Description
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 viashell) - Install/channel: latest (npm)
- Active plugins: two local plugins (
shell-env-fix,codearts-provider) — unrelated to this bug
Reproduction
- 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 }
}
}
}
}- 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.
- Create a session whose primary model is a different provider and send a single message.
/api/agent/titleshows the title agent correctly resolves tollama.cpp/MiniCPM-V4.6, but no request ever arrives at the endpoint. Verified against a llama-swap proxy: the backend model staysunloadedwhile 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>withpackage: "@opencode/ai/providers/openai-compatible"andsettings.baseURL. After the rewrite,GET /api/provider/llama.cppshows 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.baseURLbeing ignored (request toundefined/chat/completions) when noapifield is set. This issue is theapi-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
opencodejr commentedon Oct 28, 2026
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:
~/.config/opencode/opencode.jsoncwith"api": "openai"andoptions.baseURL: "http://127.0.0.1:4000/v1".opencode api --standalone get /api/config.On both builds, the migrated config shows
providers.llama.cpp.settingsas{"apiKey":"…","baseURL":"openai"}, so the endpoint fromoptions.baseURLis lost.The cause is the one you pointed to:
migrateStandardProviderandmigrateModelinpackages/core/src/v1/config/migrate.tslet the legacyapivalue overrideoptions.baseURL. That code hasn't changed ondev.The script below reproduces it. Until it's fixed, your workaround of writing the provider in the native V2 shape (
providers.<id>withpackageandsettings.baseURL) avoids the migration. Related: #36834 (options.baseURLignored when noapifield is set).Reproduction script (repro.sh)
Run it:
bash repro.sh