Repository navigation
Description
Summary
A model whose configured name is an explicit empty string ("name": "") is rendered with a blank title in the model picker, and because the picker's fuzzysort keys are title and category, the model also becomes unsearchable. Omitting name entirely works correctly, so the two spellings of "no name" behave differently. The cause is that the display-name fallback uses nullish coalescing (??) rather than rejecting empty strings.
Environment
- opencode version: 2.0.28
- OS: Windows_NT 10.0.26300 (win32 x64)
- Terminal: Windows Terminal (
WT_SESSIONset),TERM=xterm-256color - Shell:
C:\WINDOWS\system32\cmd.exe(agent shell: PowerShell 7.6.6) - Install/channel:
latest, bun global install - Active plugins:
git-ai,utf8-encoding(both local)
Reproduction
-
Create a config with three models that differ only in how "no name" is expressed:
{ "providers": { "nametest": { "name": "Name Test", "package": "@opencode/ai/providers/openai-compatible", "settings": { "baseURL": "http://127.0.0.1:9/v1" }, "models": { "alpha-none": {}, // name key absent "beta-empty": { "name": "" }, // explicit empty string "gamma-set": { "name": "Gamma Display" } } } } } -
Read the catalog as the server reports it (use
--standalonewith the directory as cwd so the project config is picked up):opencode api --standalone get /api/model
-
Open the model picker (
/models, or Ctrl+X then M) and look at the three rows, then typealpha,beta, orgammainto the filter.
Expected Behavior
All three spellings of "no name" should behave the same: a model without a usable display name should fall back to its model ID. So alpha-none and beta-empty should both display and match as alpha-none / beta-empty.
Actual Behavior
The server returns different names for the two spellings:
id=alpha-none name=[alpha-none] <- absent name falls back to the model ID
id=beta-empty name=[] <- empty string is kept as-is, no fallback
id=gamma-set name=[Gamma Display]
Consequently beta-empty renders as a blank row and cannot be found by typing beta-empty (or anything else), because fuzzysort scores title and category, and both are empty.
Additional Context
This is a regression from V1
V1 treated an empty name as "no name" and fell back to the model ID. V1 (v1.18.35, packages/opencode/src/provider/provider.ts, in the "extend database from config" loop) used a truthy test:
const name = iife(() => {
if (model.name) return model.name // "" is falsy -> falls through
if (model.id && model.id !== modelID) return modelID
return existingModel?.name ?? modelID // -> model ID for a non-catalog model
})So a config model declared as {"gpt-6-sol": {"name": ""}} was labelled gpt-6-sol in V1.
V2 replaced that with nullish coalescing, which keeps "":
// minified in the 2.0.28 build; previously ej, now aj/CQ
name: a.name ?? r?.name ?? a.idi.e. model.name ?? existingModel?.name ?? modelID. The truthy guard is gone, so the empty string now survives and reaches the UI. The removed middle branch (model.id !== modelID -> use the config key as the label) is a secondary difference.
Cause
The display-name normalization uses ??, which only substitutes null/undefined and therefore accepts "" as a real name:
function TU(e, t) {
let o = e?.find((d) => d.id === t.providerID);
return {
provider: o?.name ?? t.providerID,
model: o?.models[t.modelID]?.name ?? t.modelID, // <- "" survives here
};
}The same fallback is used for the model \xb7 provider label:
function ux(e, t, o) {
let d = TU(o, e), n = t ? ` \xb7 ${t}` : "";
return `${d.model} \xb7 ${d.provider}${n}`;
}And the picker row takes the name with no fallback at all, then searches on it:
title: Y.name,
...
fuzzysort.go(query, items, { keys: ["title", "category"] })So an empty string produces an empty title, an empty model \xb7 provider label, and no searchable key. The provider name has the same ?? shape, so a provider declared with "name": "" would be unlabeled as well.
An explicit empty string is not documented as meaningful, and the schema does not distinguish it from an omitted name. Treating "" as "a name that happens to be empty" is almost certainly unintended; something like name?.trim() ? name : modelID (or name || modelID) would make the two spellings consistent.
How this is normally reached
V1 configuration files in the wild contain "name": "" (that is where this was observed), and the V1 -> V2 migration copies it through unchanged, so the state survives an upgrade. It also appears when a config is generated by probing a gateway's /v1/models endpoint, which returns IDs but no display names, so a placeholder empty name gets written.
Minimal workaround: delete the name key (or give it a real value). The presence of the key with an empty value is the problem.
Related issue
A separate, independent defect in the same dialog: provider grouping and provider names are gated on integrations.some(i => i.connections.length > 0), so providers declared in opencode.json (the documented way to configure a custom gateway) never show grouping or provider names. Reported separately.
Activity
Ivony commentedon Oct 14, 2026
Related: #55288 (provider grouping/names are gated on
integrations.some(i => i.connections.length > 0), so config-declared providers show no grouping).opencode-agent commentedon Oct 14, 2026
Thanks for the detailed report. I was able to reproduce this on 2.0.28 with your three-model config:
opencode api --standalone get /api/modelreturnsalpha-none→"alpha-none",beta-empty→""andgamma-set→"Gamma Display"./models,beta-emptyappears as a blank row. Filteringalphaorgammafinds those models, but filteringbetashows "No results found".The same thing happens on the latest
dev(6dc4d4e): the recording and screenshots below show the blank highlighted row and the empty search result, and the script below reproduces it. As you noted, deleting the emptynamekey works around it for now.Your separate grouping issue is tracked in #55288 (see also #50322).
recording.mp4
Reproduction script (drive.ts)
Run it with opencode-drive:
opencode-drive run ./drive.ts. It drivesopencodefrom yourPATH; setOC_DEVto the path of an OpenCode checkout to run that from source instead.