From 7897f65a94d2c085896adc187f06f28d69ce7748 Mon Sep 17 00:00:00 2001 From: kshitij <82637225+kshitijk4poor@users.noreply.github.com> Date: Fri, 24 Apr 2026 03:33:05 -0700 Subject: [PATCH] fix(normalize): lowercase Xiaomi model IDs for case-insensitive config (#15066) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Xiaomi's API (api.xiaomimimo.com) requires lowercase model IDs like "mimo-v2.5-pro" but rejects mixed-case names like "MiMo-V2.5-Pro" that users copy from marketing docs or the ProviderEntry description. Add _LOWERCASE_MODEL_PROVIDERS set and apply .lower() to model names for providers in this set (currently just xiaomi) after stripping the provider prefix. This ensures any case variant in config.yaml is normalized before hitting the API. Other providers (minimax, zai, etc.) are NOT affected — their APIs accept mixed case (e.g. MiniMax-M2.7). --- hermes_cli/model_normalize.py | 19 ++++++++- tests/hermes_cli/test_xiaomi_provider.py | 54 ++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/hermes_cli/model_normalize.py b/hermes_cli/model_normalize.py index 76dace065..38f791914 100644 --- a/hermes_cli/model_normalize.py +++ b/hermes_cli/model_normalize.py @@ -100,6 +100,15 @@ _MATCHING_PREFIX_STRIP_PROVIDERS: frozenset[str] = frozenset({ "custom", }) +# Providers whose APIs require lowercase model IDs. Xiaomi's +# ``api.xiaomimimo.com`` rejects mixed-case names like ``MiMo-V2.5-Pro`` +# that users might copy from marketing docs — it only accepts +# ``mimo-v2.5-pro``. After stripping a matching provider prefix, these +# providers also get ``.lower()`` applied. +_LOWERCASE_MODEL_PROVIDERS: frozenset[str] = frozenset({ + "xiaomi", +}) + # --------------------------------------------------------------------------- # DeepSeek special handling # --------------------------------------------------------------------------- @@ -347,6 +356,9 @@ def normalize_model_for_provider(model_input: str, target_provider: str) -> str: >>> normalize_model_for_provider("claude-sonnet-4.6", "zai") 'claude-sonnet-4.6' + + >>> normalize_model_for_provider("MiMo-V2.5-Pro", "xiaomi") + 'mimo-v2.5-pro' """ name = (model_input or "").strip() if not name: @@ -410,7 +422,12 @@ def normalize_model_for_provider(model_input: str, target_provider: str) -> str: # --- Direct providers: repair matching provider prefixes only --- if provider in _MATCHING_PREFIX_STRIP_PROVIDERS: - return _strip_matching_provider_prefix(name, provider) + result = _strip_matching_provider_prefix(name, provider) + # Some providers require lowercase model IDs (e.g. Xiaomi's API + # rejects "MiMo-V2.5-Pro" but accepts "mimo-v2.5-pro"). + if provider in _LOWERCASE_MODEL_PROVIDERS: + result = result.lower() + return result # --- Authoritative native providers: preserve user-facing slugs as-is --- if provider in _AUTHORITATIVE_NATIVE_PROVIDERS: diff --git a/tests/hermes_cli/test_xiaomi_provider.py b/tests/hermes_cli/test_xiaomi_provider.py index 7205cf5a2..aa82bd48a 100644 --- a/tests/hermes_cli/test_xiaomi_provider.py +++ b/tests/hermes_cli/test_xiaomi_provider.py @@ -195,6 +195,26 @@ class TestXiaomiNormalization: from hermes_cli.model_normalize import _MATCHING_PREFIX_STRIP_PROVIDERS assert "xiaomi" in _MATCHING_PREFIX_STRIP_PROVIDERS + def test_lowercase_model_provider(self): + """Xiaomi must be in _LOWERCASE_MODEL_PROVIDERS.""" + from hermes_cli.model_normalize import _LOWERCASE_MODEL_PROVIDERS + assert "xiaomi" in _LOWERCASE_MODEL_PROVIDERS + + def test_lowercase_subset_of_matching_prefix(self): + """_LOWERCASE_MODEL_PROVIDERS must be a subset of _MATCHING_PREFIX_STRIP_PROVIDERS. + + Otherwise the .lower() code path is unreachable dead code — the + provider check at line 422 gates entry to the block. + """ + from hermes_cli.model_normalize import ( + _LOWERCASE_MODEL_PROVIDERS, + _MATCHING_PREFIX_STRIP_PROVIDERS, + ) + assert _LOWERCASE_MODEL_PROVIDERS.issubset(_MATCHING_PREFIX_STRIP_PROVIDERS), ( + f"_LOWERCASE_MODEL_PROVIDERS has entries not in _MATCHING_PREFIX_STRIP_PROVIDERS: " + f"{_LOWERCASE_MODEL_PROVIDERS - _MATCHING_PREFIX_STRIP_PROVIDERS}" + ) + def test_normalize_strips_provider_prefix(self): from hermes_cli.model_normalize import normalize_model_for_provider result = normalize_model_for_provider("xiaomi/mimo-v2-pro", "xiaomi") @@ -205,6 +225,40 @@ class TestXiaomiNormalization: result = normalize_model_for_provider("mimo-v2-pro", "xiaomi") assert result == "mimo-v2-pro" + @pytest.mark.parametrize("empty_input", ["", None, " "]) + def test_normalize_empty_and_none(self, empty_input): + """None, empty, and whitespace-only inputs return empty string.""" + from hermes_cli.model_normalize import normalize_model_for_provider + result = normalize_model_for_provider(empty_input, "xiaomi") + assert result == "" + + @pytest.mark.parametrize("input_name,expected", [ + ("MiMo-V2.5-Pro", "mimo-v2.5-pro"), + ("MIMO-V2.5-PRO", "mimo-v2.5-pro"), + ("MiMo-v2.5-pro", "mimo-v2.5-pro"), + ("mimo-v2.5-pro", "mimo-v2.5-pro"), # already lowercase + ("MiMo-V2-Pro", "mimo-v2-pro"), + ("MiMo-V2-Omni", "mimo-v2-omni"), + ("MiMo-V2-Flash", "mimo-v2-flash"), + ("MiMo-V2.5", "mimo-v2.5"), + ]) + def test_normalize_lowercases_mixed_case(self, input_name, expected): + """Xiaomi's API requires lowercase model IDs — mixed case from docs must be lowered.""" + from hermes_cli.model_normalize import normalize_model_for_provider + result = normalize_model_for_provider(input_name, "xiaomi") + assert result == expected + + @pytest.mark.parametrize("input_name,expected", [ + ("xiaomi/MiMo-V2.5-Pro", "mimo-v2.5-pro"), + ("xiaomi/MIMO-V2.5-PRO", "mimo-v2.5-pro"), + ("xiaomi/mimo-v2.5-pro", "mimo-v2.5-pro"), + ]) + def test_normalize_strips_prefix_and_lowercases(self, input_name, expected): + """Provider prefix stripping AND lowercasing must both work together.""" + from hermes_cli.model_normalize import normalize_model_for_provider + result = normalize_model_for_provider(input_name, "xiaomi") + assert result == expected + # ============================================================================= # URL mapping