Skip to content

Fix _norm_model_id to properly strip provider prefixes#1454

Closed
happy5318 wants to merge 1 commit intonesquena:masterfrom
happy5318:fix-model-id-normalization
Closed

Fix _norm_model_id to properly strip provider prefixes#1454
happy5318 wants to merge 1 commit intonesquena:masterfrom
happy5318:fix-model-id-normalization

Conversation

@happy5318
Copy link
Copy Markdown
Contributor

Summary

Fixes #1453

The _norm_model_id function was using split(':', 1)[1] which only removed the first colon-separated segment, leaving provider names in the normalized model ID.

Problem

For @custom:jingdong:GLM-5:

  • Before: jingdong:glm.5 (incorrect)
  • After: glm.5 (correct)

This caused the default model injection check to fail, resulting in a duplicate "Default" group being added to the model list.

Changes

  1. _norm_model_id: Use split(':')[-1] to get the last segment after all colons
  2. _norm_model_id: Use split('/')[-1] consistently for slash-separated paths
  3. Default model check: Replace local _norm lambda with _norm_model_id function call

Testing

  • All 20 existing tests in tests/test_model_resolver.py pass
  • Verified fix with custom provider configuration

Before

Model dropdown would show:

jingdong:
  @custom:jingdong:GLM-5
Default:
  GLM-5  <-- Duplicate!

After

Model dropdown correctly shows:

jingdong:
  @custom:jingdong:GLM-5

The _norm_model_id function was using split(':', 1)[1] which only removed
the first colon-separated segment, leaving provider names in the normalized
model ID. For example, '@Custom:jingdong:GLM-5' became 'jingdong:glm.5'
instead of 'glm.5'.

This caused the default model injection check to fail, resulting in a
duplicate 'Default' group being added to the model list even when the
model already existed with a provider prefix.

Changes:
- Use split(':')[-1] to get the last segment after all colons
- Use split('/')[-1] consistently for slash-separated paths
- Replace local _norm lambda with _norm_model_id function call

Fixes duplicate Default group appearing in model dropdown when using
custom providers with @Provider:model ID format.
@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Thanks for filing the matching issue + PR with reproduction details — the bug is real and the diagnosis matches what I see in api/config.py.

One concern with the proposed fix that I'd want addressed before merge: split(':')[-1] is too aggressive for model IDs that legitimately contain a colon inside the model name. The most common case is OpenRouter's free-tier suffix:

@openrouter:meta-llama/llama-3.1-405b:free
  • Old code (split(':', 1)[1]) → meta-llama/llama-3.1-405b:free → after / split + -.llama.3.1.405b:free
  • New code (split(':')[-1]) → freefree

That collapses every :free variant to the same normalized key ("free"), which would silently dedupe distinct OpenRouter free models against each other and against any other provider whose model ends in :free. That's a worse failure mode than the duplicate "Default" group this PR is trying to fix — see also #1426 which is specifically about OpenRouter free-tier visibility.

A more conservative shape that handles @custom:provider:model without breaking @provider:model:variant:

if s.startswith("@") and ":" in s:
    parts = s.split(":")
    # @custom:<provider>:<model...> → strip both prefix segments
    if len(parts) >= 3 and parts[0] == "@custom":
        s = ":".join(parts[2:])
    else:
        # @<provider>:<model...> → strip only the @provider segment
        s = parts[1] if len(parts) == 2 else ":".join(parts[1:])

Or, if the intent is "strip every leading @…: segment", anchor on the @ specifically rather than greedy-tail:

while s.startswith("@") and ":" in s:
    s = s.split(":", 1)[1]

That second form handles @custom:jingdong:GLM-5jingdong:GLM-5 → (no leading @, loop exits) → still leaves jingdong: in. So the explicit @custom branch above is probably the right intent.

Either way, this PR would benefit from two test cases in tests/test_model_resolver.py (or wherever _norm_model_id is exercised):

  1. _norm_model_id("@custom:jingdong:GLM-5") == "glm.5" — the bug being fixed
  2. _norm_model_id("@openrouter:meta-llama/llama-3.1-405b:free") == "llama.3.1.405b:free" — regression guard

The claim that "all 20 existing tests pass" is good, but those tests don't appear to cover either of these code paths, so they wouldn't catch the regression I'm describing.

Direction is right, just want to make sure we don't trade one dedup bug for another. cc @nesquena for the merge call.

@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Thanks @happy5318 — both this fix and the frontend mirror in #1474 shipped in v0.50.267 via release PR #1475. The work landed in master via the stage-267 batch (GitHub's auto-close didn't recognize this PR's commits through the no-ff merge, so closing manually). Thanks for the persistence — the multi-segment provider-prefix bug had been latent for several releases and the trailing-empty edge case got an Opus advisor follow-up that hardened both the backend and frontend helpers symmetrically.

Closes #1453.

Thanatos-Z pushed a commit to Thanatos-Z/hermes-webui that referenced this pull request May 2, 2026
Thanatos-Z pushed a commit to Thanatos-Z/hermes-webui that referenced this pull request May 2, 2026
…w-up

- CHANGELOG.md: v0.50.267 entry detailing nesquena#1454/nesquena#1474/nesquena#1461/nesquena#1465/nesquena#1467/nesquena#1460/nesquena#1473
  + Opus advisor SHOULD-FIX trailing-empty guard for _norm_model_id
- ROADMAP.md: bump to v0.50.267, 3776 tests collected
- TESTING.md: bump header + total to 3776
- api/config.py: trailing-empty fallback in _norm_model_id (parts[-1] or s)
- static/ui.js: mirror trailing-empty fallback in _normalizeConfiguredModelKey
- tests/test_norm_model_id_trailing_empty_guard.py: 5 regression tests
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.

Bug: _norm_model_id incorrectly handles @provider:model format with multiple colons

2 participants