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