Skip to content

fix: remove redundant provider label concatenation in _deduplicate_model_ids - #1511

Closed
lost9999 wants to merge 1 commit into
nesquena:masterfrom
lost9999:fix/dedup-label-concatenation
Closed

fix: remove redundant provider label concatenation in _deduplicate_model_ids#1511
lost9999 wants to merge 1 commit into
nesquena:masterfrom
lost9999:fix/dedup-label-concatenation

Conversation

@lost9999

@lost9999 lost9999 commented May 3, 2026

Copy link
Copy Markdown
Contributor

Problem

When multiple providers expose models with the same name (e.g., DeepSeek V4 Flash is available from Xiaomi, Ollama, HuggingFace, etc.), the _deduplicate_model_ids() function appends the provider name to the model label for every duplicate found. This results in garbled labels like:

Deepseek V4 Flash (Xiaomi) (Ollama) (HuggingFace) (Google-Gemini-Cli)

The label should reflect the model's own metadata, not accumulate provider names.

Root Cause

In api/config.py, the _deduplicate_model_ids function (around line 945) appends (provider_name) to model["label"] when deduplicating models with the same name. Since the function iterates across all provider groups and appends the label for every match, models exposed by many providers get increasingly garbled labels.

Fix

Remove the label concatenation logic entirely. The ID deduplication (prefixing with @provider_id:) is preserved — it's the correct way to differentiate same-named models from different providers. The label should be left as-is from the model's own metadata.

Before

model["id"] = f"@{pid}:{original_id}"
provider_name = group.get("provider", pid)
if model.get("label") != original_id:
    model["label"] = f"{model['label']} ({provider_name})"
else:
    model["label"] = f"{original_id} ({provider_name})"

After

model["id"] = f"@{pid}:{original_id}"

Testing

  • Verified /api/models returns clean labels with no accumulated provider suffixes
  • Model list in WebUI displays correctly with 11 clean provider groups
  • No regression in model switching or provider routing

…del_ids

The _deduplicate_model_ids function appended provider names to model
labels during deduplication, causing garbled labels like
'Deepseek V4 Flash (Xiaomi) (Ollama) (HuggingFace)' when multiple
providers exposed models with the same name.

This patch removes the label concatenation logic while preserving the
model ID deduplication (prefixing with @provider_id:). The label
should reflect the model's own metadata, not accumulated provider names.

Fixes garbled model labels in WebUI model list when multiple providers
have overlapping model names.
nesquena-hermes added a commit that referenced this pull request May 3, 2026
v0.50.277 — model-picker shared-reference fix (supersedes #1511)
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Thank you @lost9999 for filing this — your bug report was the seed for v0.50.277, shipped via release #1515. Closing this PR but only because the same fix shipped at a different layer, and Co-authored-by: lost9999 on the merge commit preserves your attribution for catching it.

Why we landed the fix differently

I traced the bug from first principles before merging. The dedup function in _deduplicate_model_ids() only ever appends ONE provider name per location it visits — so it cannot accumulate four parentheticals ((Xiaomi) (Ollama) (HuggingFace) (Google-Gemini-Cli)) on its own. That made me suspicious that removing the label-suffix logic would only paper over the symptom.

I wrote a Python repro and tested three theories:

Scenario Symptom
Independent dicts per group One parenthetical per group ✓ (correct)
Shared dict reference across groups N-1 parentheticals accumulate ✗ (matches vishnu's report)
Function called twice Idempotent (the @ guard at line 930 catches it)

The shared-reference scenario reproduces the exact symptom. I then traced the 5 group-build paths in get_models_grouped() and found:

  • OpenRouter / ollama-cloud / _PROVIDER_MODELS / named-custom paths all already build independent dicts ✓
  • The unconfigured-provider fall-through at api/config.py:2078 shares auto_detected_models across all groups that hit that branch

So when (for example) Ollama, HuggingFace, and a custom OpenAI-compat endpoint all auto-detect together, all three groups point to the SAME list with the SAME dicts inside. When dedup mutates those dicts to add @provider_id: prefixes, every group sees every other group's mutation.

The hidden bug your PR would not have fixed

Removing the label-suffix logic in _deduplicate_model_ids() makes the visible labels look clean (Deepseek V4 Flash). But the id field is corrupted by the same mechanism: all groups end up with @xiaomi:deepseek-v4-flash (whichever provider_id won the alphabetical-first race). The user clicks "Deepseek V4 Flash" under the Ollama group and the request silently routes to Xiaomi. Nobody reports this as a bug because the user can't tell — the model just behaves like a different model.

Your symptom report (label clutter) was the visible tip of a deeper iceberg (silent mis-routing). The proper fix has to be at the assignment site, not the dedup function.

What shipped

api/config.py:2078 now wraps auto_detected_models in copy.deepcopy() when assigning to a group, so each group gets its own independent dicts and dedup mutation cannot bleed across groups. The existing _deduplicate_model_ids() logic is unchanged.

Single-parenthetical disambiguation in labels is retained because the composer chip at static/index.html:441 shows the model label without the optgroup header context — Deepseek V4 Flash (Ollama) is more useful there than ambiguous Deepseek V4 Flash.

4 regression tests in tests/test_issue1511_dedup_shared_reference.py pin both the contract (independent dicts produce correct ids/labels) and the production path (get_models_grouped() actually emits independent dicts — checked via inspect.getsource() for the literal copy.deepcopy() call).

3925 → 3929 tests passing.

Why this matters for you

If you have multiple unconfigured auto-detected providers in your config, this release fixes silent model-routing. Pre-fix, selecting a duplicate model under any group routed the request to the alphabetical-first provider regardless of which group you clicked. Post-fix, requests route to the correct provider. You may notice your model selections behave differently after upgrading — that's the routing being correct, not a regression.

Thanks again for the report. The fix wouldn't have happened without your symptom description.

SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
…dup bleed-across (nesquena#1511 root cause)

Supersedes contributor PR nesquena#1511 (lost9999), which removed the label-suffix
logic in _deduplicate_model_ids() but left the underlying shared-reference
bug intact — IDs would still be silently corrupted across provider groups,
just with cleaner-looking labels.

## Bug shape

When multiple unconfigured providers (Ollama / HuggingFace / custom
endpoints / Google Gemini CLI / Xiaomi / etc.) all fell through to the
'else' branch in api/config.py:get_models_grouped() that ends with:

    groups.append({..., "models": auto_detected_models})

every group ended up sharing the SAME list reference AND the SAME dicts
inside. When _deduplicate_model_ids() then mutated those dicts to add
@provider_id: prefixes and provider-name parentheticals, the changes were
applied to every group that referenced the same dict.

Visible symptom: user 'vishnu' reported the dropdown showing
'Deepseek V4 Flash (Xiaomi) (Ollama) (HuggingFace) (Google-Gemini-Cli)'
on every group. Hidden symptom (worse): the 'id' field collapsed to
'@XiaoMi:deepseek-v4-flash' on every group too, so clicking the entry
under any group routed the request to Xiaomi.

## Fix

api/config.py:2078 — wrap auto_detected_models in copy.deepcopy() at the
groups.append site so each group gets its own independent dicts. The
existing _deduplicate_model_ids() logic is correct and unchanged; the
bug was in the assignment site, not the dedup function.

The single-parenthetical disambiguation in labels is retained because
the composer chip (composer-model-label) shows the model label without
the optgroup header context — 'Deepseek V4 Flash (Ollama)' is more
useful than ambiguous 'Deepseek V4 Flash' there.

## Tests

tests/test_issue1511_dedup_shared_reference.py — 3 new tests:
- test_groups_have_independent_model_lists: structural invariant pin
- test_unconfigured_providers_no_shared_dedup_bleed: end-to-end against
  the corrected code path; verifies each group gets its own @provider_id:
  prefix and exactly ONE provider parenthetical per disambiguated label
- test_shared_reference_pre_fix_demonstrates_corruption: documents the
  broken state that motivated the fix

Full suite: 3925 → 3928 passing (+3 new, 0 regressions).

Co-authored-by: lost9999 <56498264+lost9999@users.noreply.github.com>
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
v0.50.277 — model-picker shared-reference fix (supersedes nesquena#1511)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants