Repository navigation
fix(webui): catalog display names must keep the checkpoint's real name #1912
Description
Part of #1910.
Problem / Background
display_name is the label every WebUI surface prints for a model, and it is produced by rewriting the checkpoint name: src/server/webui/catalog_metadata.rs:465-470 takes the last path segment and replaces every - and _ with a space, so qwen3-0.6b-4bit renders as qwen3 0.6b 4bit and Meta-Llama-3.1-8B-Instruct-4bit as Meta Llama 3.1 8B Instruct 4bit. Observed on 2026-09-17 against 212 local checkpoints: the user never sees the directory or inference id they pass to -m and the API; two directories that differ only by - versus _ collapse to one label; typing the real name qwen3-0.6b-4bit into the library search finds nothing, because inventory() at webui/src/features/models/policy.ts:83-92 matches display_name, inference_id and architecture case-insensitively but the hyphenated form only matches inference_id, which no surface shows; and Playwright specs have to target the transformed name (webui/tests/models.spec.ts:178).
Current Behavior
The helper is called at catalog_metadata.rs:61 (router entries; model.name is the directory name for models-dir entries and owner/name such as mlx-community/retry-once for cache entries, see src/server/router_models_tests.rs:856) and at catalog_metadata.rs:177 (single-model mode). Consumers: the library row webui/src/features/models/screen.tsx:194-197, the inspector heading webui/src/features/models/inspector.tsx:56, the toolbar pill webui/src/provider-surfaces.tsx:25, the Chat picker webui/src/features/chat/chat.tsx:132, the Activity selector webui/src/features/activity/index.tsx:25, operation titles webui/src/features/models/operations.tsx:121, the settings selector webui/src/features/settings/server-settings.tsx:27, and the dialogs at webui/src/features/models/dialogs.tsx:34,88,100. Sorting uses it at policy.ts:104 and webui/src/state/reducer.ts:151; server-side q search concatenates it with inference_id at src/server/webui/catalog.rs:358-370. The delete dialog (dialogs.tsx:62-78) prints the opaque identity.id in a <code> block and only accepts that id as the token, while the body string models.library.delete_body (webui/src/features/models/strings.ts:245) says "Type the model name to confirm" and the field label models.library.confirm_name (strings.ts:250-253) says "Exact cache model ID to confirm", so the two strings disagree about what to type.
Proposed Solution
Make display_name the inference id with no character rewriting: models-dir and single-model entries get the last path segment of the directory (qwen3-0.6b-4bit), cache entries get the full owner/name (mlx-community/Qwen3-4B-4bit). Concretely, in catalog_metadata.rs:465-470 drop the .replace(['-', '_'], " "), and for RouterModelSource::Cache return model.name unchanged instead of the rsplit('/') segment. Do that by giving the helper the source (fn display_name(name: &str, source: RouterModelSource) -> String) or by branching at the call site at :61; the :177 single-model path keeps the last-segment rule. Do not add a humanized variant in this issue; if one is ever wanted it goes in as a separate optional field, never as the primary label. Rejected alternative: keep the space-rewritten label and print inference_id next to it, because that leaves two names for one thing and fixes neither search nor the -/_ collision.
Schema: this is a semantic change to an existing string field, so schema_version stays webui.ui-api.v1 (src/server/router_lifecycle.rs:29). The only docs/webui/api.yaml edit is widening ModelIdentity.display_name.maxLength at docs/webui/api.yaml:1518-1522 from 128 to 256 to match inference_id at api.yaml:1513-1517, because a cache id can reach 193 characters (validRepo at policy.ts:110 allows 96 per part) and a directory name can be 255 bytes; leave the root-level display_name at api.yaml:1340-1344 alone. Add a one-line description on the field stating it is the inference id verbatim, then regenerate docs/webui/generated/ui-api.d.ts with python3 scripts/ci/check_webui_contract.py --fix (the TS type stays string, so expect no diff there).
Delete dialog: keep identity.id as the token, because the reviewed identity fence at webui/src/features/models/screen.test.tsx:312-322 exists precisely so a duplicate display name cannot authorize a delete (tests/fixtures/webui/scenarios/identity-collision.json:9,97). Fix the wording instead: models.library.delete_body (en and ko at strings.ts:245-246) must say to type the ID shown below, and models.library.confirm_name must name it as the opaque model ID printed in the dialog, so the label tells the user exactly what to type and the visible name is no longer implied.
Scope
In scope: src/server/webui/catalog_metadata.rs (helper and both call sites); src/server/webui/catalog_contract_tests.rs (new test: a models-dir name containing both - and _ and a cache owner/name both come back verbatim); docs/webui/api.yaml plus regenerated docs/webui/generated/ui-api.d.ts; docs/webui/catalog.md:23 (state that display_name is the inference id verbatim, not a humanized form); webui/src/features/models/strings.ts delete strings (en and ko); tests/fixtures/webui/scenarios/identity-collision.json:97 (alpha models dir becomes alpha; the scenario still collides on inference_id, which is its purpose); and the frontend tests and fixtures that encode space-separated names, rewritten to hyphenated ids: webui/src/state/reducer.test.ts:10, webui/src/features/models/screen.test.tsx:195,216,221, webui/src/features/models/load-profile-integration.test.tsx:19, webui/tests/performance-fixtures.ts:41, webui/tests/models.spec.ts:235. Keep the CJK long-name string models.long_name (webui/src/i18n/catalog.ts:222, tests/fixtures/webui/strings.json:262-265) exercising wrapping, but as a long hyphenated id such as Qwen3-Very-Long-Local-Checkpoint-Name-With-Mixed-English-and-한국어-모델-이름, and keep the models-wrap treatment on the row, inspector heading and dialog so a 200-character id wraps instead of overflowing.
Out of scope: any other inspector content change; the model library redesign is a separate child of #1910 that depends on this issue.
Implementation Notes
- Reuse:
model.nameandinference_idare already the verbatim ids at:61and:177; no new derivation is needed. Leavematches()atcatalog.rs:358-370andinventory()as they are; oncedisplay_name == inference_idthey match the real name by construction. - Edge cases: a name ending in
/must still fall back to the whole name via the existingunwrap_or(name); a name that already contains spaces is kept verbatim; a-directory and a_directory now produce distinct labels and distinct sort positions (localeCompareatpolicy.ts:105). - Error handling: nothing new. The server never truncates; a name over 256 characters is a schema violation that
scripts/ci/check_webui_contract.pyreports on fixtures.
Acceptance Criteria
- With
models/mlx/qwen3-0.6b-4bitin the store,GET /ui-api/v1/catalogreturnsidentity.display_name == "qwen3-0.6b-4bit", and the library row, inspector heading and toolbar pill print exactly that. - A cache entry prints
mlx-community/Qwen3-4B-4bitin those three places and in the Chat and Activity selectors. - Typing
qwen3-0.6b-4bitin the library search box returns that row; the underscore spelling does not match it. - The delete confirmation body and label both say to type the opaque model ID printed in the dialog, and the fence test at
screen.test.tsx:312-322passes unchanged. -
catalog_contract_tests.rsasserts verbatimdisplay_namefor a models-dir name with-and_and for a cacheowner/name; the test fails onmainand passes with the fix. -
schema_versionis stillwebui.ui-api.v1, andpython3 scripts/ci/check_webui_contract.pypasses with the widenedmaxLength. - Verified against the running bundled binary with a store that holds both hyphenated and underscored names, not only fixtures.
Verification
cargo test --workspace --profile test-fast --features metal,accelerate
cargo clippy --workspace --all-targets -- -D warnings && cargo fmt --all -- --check
python3 scripts/ci/check_webui_contract.py
pnpm --dir webui run unit
pnpm --dir webui run browser
pnpm --dir webui run verify-generated
Manual: cargo build --release --features metal,accelerate, then ./target/release/mlxcel-server --webui --models-dir models/mlx --api-key-file <file> --no-models-autoload (--models-dir is the router-mode discovery root that yields models_dir entries; --model-store-root is the managed cache root and yields cache entries, so test both kinds), open the WebUI, and confirm the three surfaces above, the search-by-real-name result, and the delete dialog wording. A pass is the unmodified directory name on every surface.
Activity
fix(webui): wrap model names in the capacity dialog list
docs(webui): state the single-model display name rule exactly
test(webui): search the perf catalog by the name each row prints
refactor(webui): key screen copy in the catalog and drop window.confi…
fix(webui): print the checkpoint's real name as its display name
chore(webui): regenerate the committed bundle for verbatim names
fix(webui): wrap model names in the capacity dialog list
docs(webui): state the single-model display name rule exactly
test(webui): search the perf catalog by the name each row prints
fix(webui): print the checkpoint's real name as its display name (#1920)