Files
hermes-webui/api
happy5318 5518e4ceca fix(config): drop synthetic `{provider}/{model}` alias from configured-model badges (#7290)
## Thinking Path

When the active provider is a built-in plugin provider (e.g. ``commandcode``)
and the configured default model id contains a slash (e.g.
``deepseek/deepseek-v4-flash``), ``_build_configured_model_badges`` and
``_configured_model_badges_from_static_catalog`` used to publish three
raw_candidates for the same configured identity:

  - the bare native id (``deepseek/deepseek-v4-flash``)
  - a slash-prefixed alias (``commandcode/deepseek/deepseek-v4-flash``)
  - the ``@provider:model``-prefixed form (``@commandcode:deepseek/deepseek-v4-flash``)

The slash-prefixed alias was synthetic — the catalog never actually
exposed that key. The WebUI dropdown rendered it as a real option,
selecting it sent ``model=commandcode/deepseek/deepseek-v4-flash`` to
the agent, and the provider rejected it with ``HTTP 400: Model ...
is not supported on this endpoint``. The session also persisted the
wrong id, so reload replayed the failure.

Verified by reading the live source at master: the catalog exposes
only the bare native id for built-in plugin providers; the
slash-prefixed alias was a three-line entry in the ``raw_candidates``
tuple. Maintainer's 9/25 pickup contract on the issue explicitly
prohibits positional slash-splitting for ownership inference: slashes
are valid in model ids (multi-slash, URI-scheme, host:port). The fix
drops only the synthetic prefix form; the bare native id (the actual
catalog form) and the documented ``@provider:model`` prefix stay.

## What Changed

1. ``api/config.py:8617`` (``_build_configured_model_badges``) — drop
   the ``f"{provider}/{model}"`` entry from the ``raw_candidates``
   tuple. The tuple now contains only ``model`` and
   ``f"@{provider}:{model}"``. Also type-annotate the local list.
2. ``api/config.py:6946`` (``_configured_model_badges_from_static_catalog``)
   — same one-line change for the static-catalog fallback path so the
   two builders stay in sync.

The match_id / norm_lookup / exact_match logic downstream is unchanged
— those already prefer the matching provider, and with the synthetic
alias gone the bare native id is the only remaining self-collision
candidate (which the existing ``test_duplicate_slash_id_primary_badge_sticks_to_matching_provider_only``
already pins).

## Why It Matters

A user picking the configured default model in the dropdown sees two
visually identical entries for the same model id, picks the wrong
"prefixed" one (the one matching the dropdown's order), the agent
returns 400, and the session state is corrupted with a non-functional
model id until the user manually clears it. The dropdown's own
deduplication already handles `@provider:model` correctly via the
documented prefix form; only the synthesised ``{provider}/{model}``
form was wrong.

## Verification

- ``tests/test_issue7290_no_slash_prefixed_alias.py`` (new, 10 tests):
  - ``test_commandcode_deepseek_does_not_emit_slash_prefixed_alias`` —
    the literal repro from the issue: ``commandcode/deepseek/deepseek-v4-flash``
    MUST NOT appear in the badge map; the bare id ``deepseek/deepseek-v4-flash``
    is the primary badge.
  - ``test_commandcode_deepseek_at_prefixed_alias_still_present`` — the
    ``@provider:model`` form is preserved.
  - ``test_duplicate_slash_id_does_not_pollute_other_provider`` — per
    contract 4: dedupe only when provider + full native model match.
  - ``test_non_slash_models_unchanged`` (parametrized × 4) — the bare,
    @-prefixed, URI-scheme, and URL model id forms all keep working.
  - ``test_synthetic_slash_prefix_alias_is_not_in_source`` (parametrized × 2)
    — the synthesised ``f"{provider}/{model}"`` literal MUST NOT appear
    in either builder; a silent reversion to the bug fails the suite.
  - ``test_raw_candidates_only_holds_bare_and_at_prefix`` — pins the
    shape of the ``for candidate in (...)`` tuple so a future
    regression that adds any *other* synthetic prefix form
    (``f"{provider}::{model}"``, etc.) also fails.
- ``tests/test_model_picker_badges.py::test_available_models_exposes_primary_and_fallback_badges``
  updated to assert the bare-id form (``claude-haiku-4.5``) and the
  ``@provider:model`` form (``@anthropic:claude-haiku-4.5``) carry the
  fallback badge; the deleted slash-prefixed form is explicitly
  asserted absent. All other test_model_picker_badges cases unchanged.
- The 4 sibling tests
  (``test_issue3360_multi_slash_model_collision``,
  ``test_issue3429_uri_scheme_model_ids``,
  ``test_issue3959_model_suffix_dedup``,
  ``test_norm_model_id_trailing_empty_guard``) still pass — they pin
  ``_norm_model_id`` behaviour which is unchanged by this PR.
- ``pytest tests/ -k 'model or picker or badge'``: 1507 passed,
  1 skipped, 2 failed. The 2 failures (test_issue1426_free_tier,
  test_issue2245_mixed_case) are the documented upstream shard-race
  flake — both pass when run individually (verified). Pre-existing
  per issue #7656's notes.
- ``ruff check api/config.py tests/test_issue7290_no_slash_prefixed_alias.py``:
  clean on the new test file. (The 4 pre-existing F811/import-order
  findings in api/config.py are on untouched lines.)

## Contract Change

No API contract change. The ``/api/models`` payload's
``configured_model_badges`` map drops one synthetic key per configured
entry (the slash-prefixed alias). Real consumers that look up a badge
by exact native id or by ``@provider:model`` are unaffected. The
dropdown's existing dedupe is now correct without a manual workaround.

## Risks

- A consumer that depended on the slash-prefixed alias as a stable
  lookup key would need to switch to the bare native id. No consumer
  in this repo matches that pattern — ``test_duplicate_slash_id_*
  _primary_badge_sticks_to_matching_provider_only`` was already
  asserting that the bare id is the canonical lookup.
- The dropdown's option value (the string sent as ``model=`` to the
  agent) is the bare native id; the test runner, the session
  restore path, and the request payload all use the bare id
  consistently per maintainer contract 3.

## Model Used

Hermes Agent (step-5-preview) with pytest, ruff. Tests execute the
live ``_build_configured_model_badges`` from the production source
(``exec`` the function body with inline ``_norm_model_id``), so a
revert to the original ``raw_candidates`` tuple fails the suite
immediately on the source-shape test.

## Related

- #7290 — original issue (Porter-Key, 2026-08-25) with the full repro
  and the maintainer's 9/25 pickup contract (6 items).
- #3478 — named custom-provider ``@custom:name:model`` prefix.
- #7241, #6884 — adjacent ``@provider:model`` prefix lanes.
- #3360 — multi-slash model id collision (the ``_norm_model_id``
  fix predates this PR and is preserved).
- #3429 — URI-scheme model id preservation (also preserved).
- #3959 — colon-suffix model id preservation (also preserved).
2026-09-22 10:10:10 +00:00
..