Skip to content

models: empty configured name renders blank and is unsearchable (V1 -> V2 regression: truthy check became ??) #55289

Closed

Description

@Ivony

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_SESSION set), 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

  1. 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" }
          }
        }
      }
    }
  2. Read the catalog as the server reports it (use --standalone with the directory as cwd so the project config is picked up):

    opencode api --standalone get /api/model
  3. Open the model picker (/models, or Ctrl+X then M) and look at the three rows, then type alpha, beta, or gamma into 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.id

i.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

  1. Ivony commented on Oct 14, 2026

    @Ivony
    Author

    Related: #55288 (provider grouping/names are gated on integrations.some(i => i.connections.length > 0), so config-declared providers show no grouping).

  2. opencode-agent commented on Oct 14, 2026

    @opencode-agent
    Contributor

    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/model returns alpha-none → "alpha-none", beta-empty → "" and gamma-set → "Gamma Display".
    • In /models, beta-empty appears as a blank row. Filtering alpha or gamma finds those models, but filtering beta shows "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 empty name key works around it for now.

    Your separate grouping issue is tracked in #55288 (see also #50322).

    recording.mp4

    Screenshot

    Screenshot

    Reproduction script (drive.ts)
    // Repro for anomalyco/opencode#55289: a model with "name": "" renders blank in /models and can't be found by filtering.
    import { Effect } from "effect"
    import { OpenCodeDriver } from "opencode-drive"
    
    export default OpenCodeDriver.use(
      {
        ...(process.env.OC_DEV ? { opencode: { dev: process.env.OC_DEV } } : {}),
        tui: { recording: true, keypressOverlay: true },
        config: {
          providers: {
            nametest: {
              name: "Name Test",
              package: "@opencode/ai/providers/openai-compatible",
              settings: { baseURL: "http://127.0.0.1:9/v1" },
              models: {
                "alpha-none": {},
                "beta-empty": { name: "" },
                "gamma-set": { name: "Gamma Display" },
              },
            },
          },
        } as any,
      },
      ({ ui }) =>
        Effect.gen(function* () {
          yield* Effect.sleep(2000)
          yield* ui.type("/models")
          yield* Effect.sleep(800)
          yield* ui.enter()
          yield* Effect.sleep(1500)
          yield* ui.screenshot("list-unfiltered")
          yield* ui.type("alpha")
          yield* Effect.sleep(1200)
          yield* ui.screenshot("filter-alpha")
          yield* ui.press("backspace", {}) ; yield* ui.press("backspace", {}); yield* ui.press("backspace", {}); yield* ui.press("backspace", {}); yield* ui.press("backspace", {})
          yield* ui.type("gamma")
          yield* Effect.sleep(1200)
          yield* ui.screenshot("filter-gamma")
          yield* ui.press("backspace", {}) ; yield* ui.press("backspace", {}); yield* ui.press("backspace", {}); yield* ui.press("backspace", {}); yield* ui.press("backspace", {})
          yield* ui.type("beta")
          yield* Effect.sleep(1500)
          yield* ui.screenshot("filter-beta")
        }),
    )

    Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.

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