/ tests / hermes_cli / test_xiaomi_provider.py
test_xiaomi_provider.py
  1  """Tests for Xiaomi MiMo provider support."""
  2  
  3  import os
  4  
  5  import pytest
  6  
  7  from hermes_cli.auth import (
  8      PROVIDER_REGISTRY,
  9      resolve_provider,
 10      get_api_key_provider_status,
 11      resolve_api_key_provider_credentials,
 12      AuthError,
 13  )
 14  
 15  
 16  # =============================================================================
 17  # Provider Registry
 18  # =============================================================================
 19  
 20  
 21  class TestXiaomiProviderRegistry:
 22      """Verify Xiaomi is registered correctly in the PROVIDER_REGISTRY."""
 23  
 24      def test_registered(self):
 25          assert "xiaomi" in PROVIDER_REGISTRY
 26  
 27      def test_name(self):
 28          assert PROVIDER_REGISTRY["xiaomi"].name == "Xiaomi MiMo"
 29  
 30      def test_auth_type(self):
 31          assert PROVIDER_REGISTRY["xiaomi"].auth_type == "api_key"
 32  
 33      def test_inference_base_url(self):
 34          assert PROVIDER_REGISTRY["xiaomi"].inference_base_url == "https://api.xiaomimimo.com/v1"
 35  
 36      def test_api_key_env_vars(self):
 37          assert PROVIDER_REGISTRY["xiaomi"].api_key_env_vars == ("XIAOMI_API_KEY",)
 38  
 39      def test_base_url_env_var(self):
 40          assert PROVIDER_REGISTRY["xiaomi"].base_url_env_var == "XIAOMI_BASE_URL"
 41  
 42  
 43  # =============================================================================
 44  # Aliases
 45  # =============================================================================
 46  
 47  
 48  class TestXiaomiAliases:
 49      """All aliases should resolve to 'xiaomi'."""
 50  
 51      @pytest.mark.parametrize("alias", [
 52          "xiaomi", "mimo", "xiaomi-mimo",
 53      ])
 54      def test_alias_resolves(self, alias, monkeypatch):
 55          # Clear env to avoid auto-detection interfering
 56          for key in ("XIAOMI_API_KEY",):
 57              monkeypatch.delenv(key, raising=False)
 58          monkeypatch.setenv("XIAOMI_API_KEY", "sk-test-key-12345678")
 59          assert resolve_provider(alias) == "xiaomi"
 60  
 61      def test_normalize_provider_models_py(self):
 62          from hermes_cli.models import normalize_provider
 63          assert normalize_provider("mimo") == "xiaomi"
 64          assert normalize_provider("xiaomi-mimo") == "xiaomi"
 65  
 66      def test_normalize_provider_providers_py(self):
 67          from hermes_cli.providers import normalize_provider
 68          assert normalize_provider("mimo") == "xiaomi"
 69          assert normalize_provider("xiaomi-mimo") == "xiaomi"
 70  
 71  
 72  # =============================================================================
 73  # Auto-detection
 74  # =============================================================================
 75  
 76  
 77  class TestXiaomiAutoDetection:
 78      """Setting XIAOMI_API_KEY should auto-detect the provider."""
 79  
 80      def test_auto_detect(self, monkeypatch):
 81          # Clear all other provider env vars
 82          for var in ("OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY",
 83                       "DEEPSEEK_API_KEY", "GOOGLE_API_KEY", "GEMINI_API_KEY",
 84                       "DASHSCOPE_API_KEY", "XAI_API_KEY", "KIMI_API_KEY",
 85                       "MINIMAX_API_KEY", "AI_GATEWAY_API_KEY", "KILOCODE_API_KEY",
 86                       "HF_TOKEN", "GLM_API_KEY", "COPILOT_GITHUB_TOKEN",
 87                       "GH_TOKEN", "GITHUB_TOKEN", "MINIMAX_CN_API_KEY",
 88                       "TOKENHUB_API_KEY", "ARCEEAI_API_KEY"):
 89              monkeypatch.delenv(var, raising=False)
 90          monkeypatch.setenv("XIAOMI_API_KEY", "sk-xiaomi-test-12345678")
 91          provider = resolve_provider("auto")
 92          assert provider == "xiaomi"
 93  
 94  
 95  # =============================================================================
 96  # Credentials
 97  # =============================================================================
 98  
 99  
100  class TestXiaomiCredentials:
101      """Test credential resolution for the xiaomi provider."""
102  
103      def test_status_configured(self, monkeypatch):
104          monkeypatch.setenv("XIAOMI_API_KEY", "sk-test-12345678")
105          status = get_api_key_provider_status("xiaomi")
106          assert status["configured"]
107  
108      def test_status_not_configured(self, monkeypatch):
109          monkeypatch.delenv("XIAOMI_API_KEY", raising=False)
110          status = get_api_key_provider_status("xiaomi")
111          assert not status["configured"]
112  
113      def test_resolve_credentials(self, monkeypatch):
114          monkeypatch.setenv("XIAOMI_API_KEY", "sk-test-12345678")
115          monkeypatch.delenv("XIAOMI_BASE_URL", raising=False)
116          creds = resolve_api_key_provider_credentials("xiaomi")
117          assert creds["api_key"] == "sk-test-12345678"
118          assert creds["base_url"] == "https://api.xiaomimimo.com/v1"
119  
120      def test_custom_base_url_override(self, monkeypatch):
121          monkeypatch.setenv("XIAOMI_API_KEY", "sk-test-12345678")
122          monkeypatch.setenv("XIAOMI_BASE_URL", "https://custom.xiaomi.example/v1")
123          creds = resolve_api_key_provider_credentials("xiaomi")
124          assert creds["base_url"] == "https://custom.xiaomi.example/v1"
125  
126  
127  # =============================================================================
128  # Model catalog (dynamic — no static list)
129  # =============================================================================
130  
131  
132  class TestXiaomiModelCatalog:
133      """Xiaomi uses dynamic model discovery via models.dev."""
134  
135      def test_models_dev_mapping(self):
136          from agent.models_dev import PROVIDER_TO_MODELS_DEV
137          assert PROVIDER_TO_MODELS_DEV["xiaomi"] == "xiaomi"
138  
139      def test_static_model_list_fallback(self):
140          """Static _PROVIDER_MODELS fallback must exist for model picker.
141  
142          We only assert the provider key is present — the specific model
143          names are data that changes with upstream releases and doesn't
144          belong in tests.
145          """
146          from hermes_cli.models import _PROVIDER_MODELS
147          assert "xiaomi" in _PROVIDER_MODELS
148          assert len(_PROVIDER_MODELS["xiaomi"]) >= 1
149  
150      def test_list_agentic_models_mock(self, monkeypatch):
151          """When models.dev returns Xiaomi data, list_agentic_models should return models."""
152          from agent import models_dev as md
153  
154          fake_data = {
155              "xiaomi": {
156                  "name": "Xiaomi",
157                  "api": "https://api.xiaomimimo.com/v1",
158                  "env": ["XIAOMI_API_KEY"],
159                  "models": {
160                      "mimo-v2-pro": {
161                          "limit": {"context": 1000000},
162                          "tool_call": True,
163                      },
164                      "mimo-v2-omni": {
165                          "limit": {"context": 256000},
166                          "tool_call": True,
167                      },
168                      "mimo-v2-flash": {
169                          "limit": {"context": 256000},
170                          "tool_call": True,
171                      },
172                  },
173              }
174          }
175          monkeypatch.setattr(md, "fetch_models_dev", lambda: fake_data)
176  
177          result = md.list_agentic_models("xiaomi")
178          assert "mimo-v2-pro" in result
179          assert "mimo-v2-flash" in result
180  
181  
182  # =============================================================================
183  # Normalization
184  # =============================================================================
185  
186  
187  class TestXiaomiNormalization:
188      """Model name normalization — Xiaomi is a direct provider."""
189  
190      def test_vendor_prefix_mapping(self):
191          from hermes_cli.model_normalize import _VENDOR_PREFIXES
192          assert _VENDOR_PREFIXES.get("mimo") == "xiaomi"
193  
194      def test_matching_prefix_strip(self):
195          """xiaomi/mimo-v2-pro should normalize to mimo-v2-pro for direct API."""
196          from hermes_cli.model_normalize import _MATCHING_PREFIX_STRIP_PROVIDERS
197          assert "xiaomi" in _MATCHING_PREFIX_STRIP_PROVIDERS
198  
199      def test_lowercase_model_provider(self):
200          """Xiaomi must be in _LOWERCASE_MODEL_PROVIDERS."""
201          from hermes_cli.model_normalize import _LOWERCASE_MODEL_PROVIDERS
202          assert "xiaomi" in _LOWERCASE_MODEL_PROVIDERS
203  
204      def test_lowercase_subset_of_matching_prefix(self):
205          """_LOWERCASE_MODEL_PROVIDERS must be a subset of _MATCHING_PREFIX_STRIP_PROVIDERS.
206  
207          Otherwise the .lower() code path is unreachable dead code — the
208          provider check at line 422 gates entry to the block.
209          """
210          from hermes_cli.model_normalize import (
211              _LOWERCASE_MODEL_PROVIDERS,
212              _MATCHING_PREFIX_STRIP_PROVIDERS,
213          )
214          assert _LOWERCASE_MODEL_PROVIDERS.issubset(_MATCHING_PREFIX_STRIP_PROVIDERS), (
215              f"_LOWERCASE_MODEL_PROVIDERS has entries not in _MATCHING_PREFIX_STRIP_PROVIDERS: "
216              f"{_LOWERCASE_MODEL_PROVIDERS - _MATCHING_PREFIX_STRIP_PROVIDERS}"
217          )
218  
219      def test_normalize_strips_provider_prefix(self):
220          from hermes_cli.model_normalize import normalize_model_for_provider
221          result = normalize_model_for_provider("xiaomi/mimo-v2-pro", "xiaomi")
222          assert result == "mimo-v2-pro"
223  
224      def test_normalize_bare_name_unchanged(self):
225          from hermes_cli.model_normalize import normalize_model_for_provider
226          result = normalize_model_for_provider("mimo-v2-pro", "xiaomi")
227          assert result == "mimo-v2-pro"
228  
229      @pytest.mark.parametrize("empty_input", ["", None, "   "])
230      def test_normalize_empty_and_none(self, empty_input):
231          """None, empty, and whitespace-only inputs return empty string."""
232          from hermes_cli.model_normalize import normalize_model_for_provider
233          result = normalize_model_for_provider(empty_input, "xiaomi")
234          assert result == ""
235  
236      @pytest.mark.parametrize("input_name,expected", [
237          ("MiMo-V2.5-Pro", "mimo-v2.5-pro"),
238          ("MIMO-V2.5-PRO", "mimo-v2.5-pro"),
239          ("MiMo-v2.5-pro", "mimo-v2.5-pro"),
240          ("mimo-v2.5-pro", "mimo-v2.5-pro"),     # already lowercase
241          ("MiMo-V2-Pro", "mimo-v2-pro"),
242          ("MiMo-V2-Omni", "mimo-v2-omni"),
243          ("MiMo-V2-Flash", "mimo-v2-flash"),
244          ("MiMo-V2.5", "mimo-v2.5"),
245      ])
246      def test_normalize_lowercases_mixed_case(self, input_name, expected):
247          """Xiaomi's API requires lowercase model IDs — mixed case from docs must be lowered."""
248          from hermes_cli.model_normalize import normalize_model_for_provider
249          result = normalize_model_for_provider(input_name, "xiaomi")
250          assert result == expected
251  
252      @pytest.mark.parametrize("input_name,expected", [
253          ("xiaomi/MiMo-V2.5-Pro", "mimo-v2.5-pro"),
254          ("xiaomi/MIMO-V2.5-PRO", "mimo-v2.5-pro"),
255          ("xiaomi/mimo-v2.5-pro", "mimo-v2.5-pro"),
256      ])
257      def test_normalize_strips_prefix_and_lowercases(self, input_name, expected):
258          """Provider prefix stripping AND lowercasing must both work together."""
259          from hermes_cli.model_normalize import normalize_model_for_provider
260          result = normalize_model_for_provider(input_name, "xiaomi")
261          assert result == expected
262  
263  
264  # =============================================================================
265  # URL mapping
266  # =============================================================================
267  
268  
269  class TestXiaomiURLMapping:
270      """Test URL → provider inference for Xiaomi endpoints."""
271  
272      def test_url_to_provider(self):
273          from agent.model_metadata import _URL_TO_PROVIDER
274          assert _URL_TO_PROVIDER.get("api.xiaomimimo.com") == "xiaomi"
275  
276      def test_provider_prefixes(self):
277          from agent.model_metadata import _PROVIDER_PREFIXES
278          assert "xiaomi" in _PROVIDER_PREFIXES
279          assert "mimo" in _PROVIDER_PREFIXES
280          assert "xiaomi-mimo" in _PROVIDER_PREFIXES
281  
282      def test_infer_from_url(self):
283          from agent.model_metadata import _infer_provider_from_url
284          assert _infer_provider_from_url("https://api.xiaomimimo.com/v1") == "xiaomi"
285  
286      def test_infer_from_regional_urls(self):
287          """Regional token-plan endpoints should also resolve to xiaomi."""
288          from agent.model_metadata import _infer_provider_from_url
289          assert _infer_provider_from_url("https://token-plan-ams.xiaomimimo.com/v1") == "xiaomi"
290          assert _infer_provider_from_url("https://token-plan-cn.xiaomimimo.com/v1") == "xiaomi"
291          assert _infer_provider_from_url("https://token-plan-sgp.xiaomimimo.com/v1") == "xiaomi"
292  
293  
294  # =============================================================================
295  # providers.py
296  # =============================================================================
297  
298  
299  class TestXiaomiProvidersModule:
300      """Test Xiaomi in the unified providers module."""
301  
302      def test_overlay_exists(self):
303          from hermes_cli.providers import HERMES_OVERLAYS
304          assert "xiaomi" in HERMES_OVERLAYS
305          overlay = HERMES_OVERLAYS["xiaomi"]
306          assert overlay.transport == "openai_chat"
307          assert overlay.base_url_env_var == "XIAOMI_BASE_URL"
308          assert not overlay.is_aggregator
309  
310      def test_alias_resolves(self):
311          from hermes_cli.providers import normalize_provider
312          assert normalize_provider("mimo") == "xiaomi"
313          assert normalize_provider("xiaomi-mimo") == "xiaomi"
314  
315      def test_label(self):
316          from hermes_cli.providers import get_label
317          assert get_label("xiaomi") == "Xiaomi MiMo"
318  
319      def test_get_provider(self):
320          pdef = None
321          try:
322              from hermes_cli.providers import get_provider
323              pdef = get_provider("xiaomi")
324          except Exception:
325              pass
326          if pdef is not None:
327              assert pdef.id == "xiaomi"
328              assert pdef.transport == "openai_chat"
329  
330  
331  # =============================================================================
332  # Auxiliary client
333  # =============================================================================
334  
335  
336  class TestXiaomiAuxiliary:
337      """Xiaomi auxiliary routing: vision → omni, non-vision → user's main model, never flash."""
338  
339      def test_no_flash_in_aux_models(self):
340          """mimo-v2-flash must NEVER be used for automatic aux routing."""
341          from agent.auxiliary_client import _API_KEY_PROVIDER_AUX_MODELS
342          assert "xiaomi" not in _API_KEY_PROVIDER_AUX_MODELS
343  
344      def test_vision_model_override(self):
345          """Xiaomi vision tasks should use mimo-v2.5 (multimodal), not the main model."""
346          from agent.auxiliary_client import _PROVIDER_VISION_MODELS
347          assert "xiaomi" in _PROVIDER_VISION_MODELS
348          assert _PROVIDER_VISION_MODELS["xiaomi"] == "mimo-v2.5"
349  
350  
351  # =============================================================================
352  # Agent init (no SyntaxError, correct api_mode)
353  # =============================================================================
354  
355  
356  class TestXiaomiDoctor:
357      """Verify hermes doctor recognizes Xiaomi env vars."""
358  
359      def test_provider_env_hints(self):
360          from hermes_cli.doctor import _PROVIDER_ENV_HINTS
361          assert "XIAOMI_API_KEY" in _PROVIDER_ENV_HINTS
362  
363  
364  class TestXiaomiAgentInit:
365      """Verify the agent can be constructed with xiaomi provider without errors."""
366  
367      def test_no_syntax_errors(self):
368          """Importing run_agent with xiaomi should not raise."""
369          import importlib
370          importlib.import_module("run_agent")
371  
372      def test_api_mode_is_chat_completions(self):
373          from hermes_cli.providers import HERMES_OVERLAYS, TRANSPORT_TO_API_MODE
374          overlay = HERMES_OVERLAYS["xiaomi"]
375          api_mode = TRANSPORT_TO_API_MODE[overlay.transport]
376          assert api_mode == "chat_completions"