fix: custom/unknown model prefixes must not be stripped on provider switch — v0.50.100 (#752)

## Summary

Regression fix for #751.

Models with custom or unrecognized prefixes (e.g. `custom-provider/my-model`, `test/import-model`) were being incorrectly replaced with the active provider default. Root cause: `_normalize_provider_id("custom-provider")` matched the `"custom"` prefix and returned `"custom"`, which ≠ `active_provider` → normalization fired.

Two-part fix:
1. Add `"custom"` and `"openrouter"` to the `model_provider` exclusion set in `_resolve_compatible_session_model` (parallel to the existing `active_provider` guard)
2. Return `""` for unknown prefixes in `_normalize_provider_id` so the `if model_provider` truthiness check safely short-circuits

Adds a regression test covering `custom-provider/`, `test/`, `my-local-llm/`, and `lmstudio-community/` prefixes.

## Tests

1499 passed, 0 failures (was 2 failures before this fix)
This commit is contained in:
nesquena-hermes
2026-04-19 23:27:24 -07:00
committed by GitHub
parent 7f16a41a31
commit 81ba420716
3 changed files with 40 additions and 2 deletions

View File

@@ -1,5 +1,10 @@
# Hermes Web UI -- Changelog
## [v0.50.100] — 2026-04-20
### Fixed
- **Session model normalization: unknown provider prefixes now pass through** — custom/unlisted model prefixes (e.g. `custom-provider/my-model`) are no longer incorrectly stripped when switching providers. Only well-known provider prefixes (`gpt-`, `claude-`, `gemini-`, etc.) are normalized. Regression introduced in v0.50.99. (#751)
## [v0.50.99] — 2026-04-20
### Fixed

View File

@@ -183,7 +183,9 @@ def _normalize_provider_id(value: str | None) -> str:
):
if raw.startswith(prefix):
return normalized
return raw
# Unknown prefix — return empty so callers treat it as "no match" and pass
# the model through unchanged rather than incorrectly stripping it.
return ""
def _resolve_compatible_session_model(model_id: str | None) -> tuple[str, bool]:
@@ -217,7 +219,9 @@ def _resolve_compatible_session_model(model_id: str | None) -> tuple[str, bool]:
return model, False
model_provider = _normalize_provider_id(model[:slash])
if model_provider and model_provider != active_provider and default_model:
# Skip normalization for models on custom/openrouter namespaces — these are
# user-controlled and should never be silently replaced.
if model_provider and model_provider not in {"", "custom", "openrouter"} and model_provider != active_provider and default_model:
return default_model, True
return model, False

View File

@@ -430,3 +430,32 @@ class TestChatStartEffectiveModelRecovery:
assert "localStorage.setItem('hermes-webui-model', startData.effective_model)" in src, (
"effective_model correction must update the saved model preference"
)
def test_unknown_prefix_model_passes_through_unchanged(monkeypatch):
"""Models with unknown/custom prefixes must never be stripped — regression test for #751."""
import api.routes as routes
monkeypatch.setattr(
routes,
"get_available_models",
lambda: {
"active_provider": "openai-codex",
"default_model": "gpt-5.4-mini",
},
)
for custom_model in (
"custom-provider/test-model-999",
"test/import-model",
"my-local-llm/variant-1",
"lmstudio-community/Qwen2.5-Coder-7B-Instruct-GGUF",
):
effective, changed = routes._resolve_compatible_session_model(custom_model)
assert changed is False, (
f"Model '{custom_model}' has an unknown prefix and must pass through unchanged, "
f"but _resolve_compatible_session_model returned changed=True (effective='{effective}')"
)
assert effective == custom_model, (
f"Expected '{custom_model}', got '{effective}'"
)