fix(normalize): lowercase Xiaomi model IDs for case-insensitive config (#15066)

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).
This commit is contained in:
kshitij 2026-04-24 03:33:05 -07:00 committed by GitHub
parent 3e994e38f7
commit 7897f65a94
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 72 additions and 1 deletions

View file

@ -100,6 +100,15 @@ _MATCHING_PREFIX_STRIP_PROVIDERS: frozenset[str] = frozenset({
"custom", "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 # 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") >>> normalize_model_for_provider("claude-sonnet-4.6", "zai")
'claude-sonnet-4.6' 'claude-sonnet-4.6'
>>> normalize_model_for_provider("MiMo-V2.5-Pro", "xiaomi")
'mimo-v2.5-pro'
""" """
name = (model_input or "").strip() name = (model_input or "").strip()
if not name: 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 --- # --- Direct providers: repair matching provider prefixes only ---
if provider in _MATCHING_PREFIX_STRIP_PROVIDERS: 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 --- # --- Authoritative native providers: preserve user-facing slugs as-is ---
if provider in _AUTHORITATIVE_NATIVE_PROVIDERS: if provider in _AUTHORITATIVE_NATIVE_PROVIDERS:

View file

@ -195,6 +195,26 @@ class TestXiaomiNormalization:
from hermes_cli.model_normalize import _MATCHING_PREFIX_STRIP_PROVIDERS from hermes_cli.model_normalize import _MATCHING_PREFIX_STRIP_PROVIDERS
assert "xiaomi" in _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): def test_normalize_strips_provider_prefix(self):
from hermes_cli.model_normalize import normalize_model_for_provider from hermes_cli.model_normalize import normalize_model_for_provider
result = normalize_model_for_provider("xiaomi/mimo-v2-pro", "xiaomi") 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") result = normalize_model_for_provider("mimo-v2-pro", "xiaomi")
assert result == "mimo-v2-pro" 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 # URL mapping