Files
hermes-agent/tests/hermes_cli/test_xiaomi_provider.py

322 lines
12 KiB
Python
Raw Normal View History

"""Tests for Xiaomi MiMo provider support."""
import os
import pytest
from hermes_cli.auth import (
PROVIDER_REGISTRY,
resolve_provider,
get_api_key_provider_status,
resolve_api_key_provider_credentials,
AuthError,
)
# =============================================================================
# Provider Registry
# =============================================================================
class TestXiaomiProviderRegistry:
"""Verify Xiaomi is registered correctly in the PROVIDER_REGISTRY."""
def test_registered(self):
assert "xiaomi" in PROVIDER_REGISTRY
def test_name(self):
assert PROVIDER_REGISTRY["xiaomi"].name == "Xiaomi MiMo"
def test_auth_type(self):
assert PROVIDER_REGISTRY["xiaomi"].auth_type == "api_key"
def test_inference_base_url(self):
assert PROVIDER_REGISTRY["xiaomi"].inference_base_url == "https://api.xiaomimimo.com/v1"
def test_api_key_env_vars(self):
assert PROVIDER_REGISTRY["xiaomi"].api_key_env_vars == ("XIAOMI_API_KEY",)
def test_base_url_env_var(self):
assert PROVIDER_REGISTRY["xiaomi"].base_url_env_var == "XIAOMI_BASE_URL"
# =============================================================================
# Aliases
# =============================================================================
class TestXiaomiAliases:
"""All aliases should resolve to 'xiaomi'."""
@pytest.mark.parametrize("alias", [
"xiaomi", "mimo", "xiaomi-mimo",
])
def test_alias_resolves(self, alias, monkeypatch):
# Clear env to avoid auto-detection interfering
for key in ("XIAOMI_API_KEY",):
monkeypatch.delenv(key, raising=False)
monkeypatch.setenv("XIAOMI_API_KEY", "sk-test-key-12345678")
assert resolve_provider(alias) == "xiaomi"
def test_normalize_provider_models_py(self):
from hermes_cli.models import normalize_provider
assert normalize_provider("mimo") == "xiaomi"
assert normalize_provider("xiaomi-mimo") == "xiaomi"
def test_normalize_provider_providers_py(self):
from hermes_cli.providers import normalize_provider
assert normalize_provider("mimo") == "xiaomi"
assert normalize_provider("xiaomi-mimo") == "xiaomi"
# =============================================================================
# Auto-detection
# =============================================================================
class TestXiaomiAutoDetection:
"""Setting XIAOMI_API_KEY should auto-detect the provider."""
def test_auto_detect(self, monkeypatch):
# Clear all other provider env vars
for var in ("OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY",
"DEEPSEEK_API_KEY", "GOOGLE_API_KEY", "GEMINI_API_KEY",
"DASHSCOPE_API_KEY", "XAI_API_KEY", "KIMI_API_KEY",
"MINIMAX_API_KEY", "AI_GATEWAY_API_KEY", "KILOCODE_API_KEY",
"HF_TOKEN", "GLM_API_KEY", "COPILOT_GITHUB_TOKEN",
"GH_TOKEN", "GITHUB_TOKEN", "MINIMAX_CN_API_KEY"):
monkeypatch.delenv(var, raising=False)
monkeypatch.setenv("XIAOMI_API_KEY", "sk-xiaomi-test-12345678")
provider = resolve_provider("auto")
assert provider == "xiaomi"
# =============================================================================
# Credentials
# =============================================================================
class TestXiaomiCredentials:
"""Test credential resolution for the xiaomi provider."""
def test_status_configured(self, monkeypatch):
monkeypatch.setenv("XIAOMI_API_KEY", "sk-test-12345678")
status = get_api_key_provider_status("xiaomi")
assert status["configured"]
def test_status_not_configured(self, monkeypatch):
monkeypatch.delenv("XIAOMI_API_KEY", raising=False)
status = get_api_key_provider_status("xiaomi")
assert not status["configured"]
def test_resolve_credentials(self, monkeypatch):
monkeypatch.setenv("XIAOMI_API_KEY", "sk-test-12345678")
monkeypatch.delenv("XIAOMI_BASE_URL", raising=False)
creds = resolve_api_key_provider_credentials("xiaomi")
assert creds["api_key"] == "sk-test-12345678"
assert creds["base_url"] == "https://api.xiaomimimo.com/v1"
def test_custom_base_url_override(self, monkeypatch):
monkeypatch.setenv("XIAOMI_API_KEY", "sk-test-12345678")
monkeypatch.setenv("XIAOMI_BASE_URL", "https://custom.xiaomi.example/v1")
creds = resolve_api_key_provider_credentials("xiaomi")
assert creds["base_url"] == "https://custom.xiaomi.example/v1"
# =============================================================================
# Model catalog (dynamic — no static list)
# =============================================================================
class TestXiaomiModelCatalog:
"""Xiaomi uses dynamic model discovery via models.dev."""
def test_models_dev_mapping(self):
from agent.models_dev import PROVIDER_TO_MODELS_DEV
assert PROVIDER_TO_MODELS_DEV["xiaomi"] == "xiaomi"
def test_static_model_list_fallback(self):
test: stop testing mutable data — convert change-detectors to invariants (#13363) Catalog snapshots, config version literals, and enumeration counts are data that changes as designed. Tests that assert on those values add no behavioral coverage — they just break CI on every routine update and cost engineering time to 'fix.' Replace with invariants where one exists, delete where none does. Deleted (pure snapshots): - TestMinimaxModelCatalog (3 tests): 'MiniMax-M2.7 in models' et al - TestGeminiModelCatalog: 'gemini-2.5-pro in models', 'gemini-3.x in models' - test_browser_camofox_state::test_config_version_matches_current_schema (docstring literally said it would break on unrelated bumps) Relaxed (keep plumbing check, drop snapshot): - Xiaomi / Arcee / Kimi moonshot / Kimi coding / HuggingFace static lists: now assert 'provider exists and has >= 1 entry' instead of specific names - HuggingFace main/models.py consistency test: drop 'len >= 6' floor Dynamicized (follow source, not a literal): - 3x test_config.py migration tests: raw['_config_version'] == DEFAULT_CONFIG['_config_version'] instead of hardcoded 21 Fixed stale tests against intentional behavior changes: - test_insights::test_gateway_format_hides_cost: name matches new behavior (no dollar figures); remove contradicting '$' in text assertion - test_config::prefers_api_then_url_then_base_url: flipped per PR #9332; rename + update to base_url > url > api - test_anthropic_adapter: relax assert_called_once() (xdist-flaky) to assert called — contract is 'credential flowed through' - test_interrupt_propagation: add provider/model/_base_url to bare-agent fixture so the stale-timeout code path resolves Fixed stale integration tests against opt-in plugin gate: - transform_tool_result + transform_terminal_output: write plugins.enabled allow-list to config.yaml and reset the plugin manager singleton Source fix (real consistency invariant): - agent/model_metadata.py: add moonshotai/Kimi-K2.6 context length (262144, same as K2.5). test_model_metadata_has_context_lengths was correctly catching the gap. Policy: - AGENTS.md Testing section: new subsection 'Don't write change-detector tests' with do/don't examples. Reviewers should reject catalog-snapshot assertions in new tests. Covers every test that failed on the last completed main CI run (24703345583) except test_modal_sandbox_fixes::test_terminal_tool_present + test_terminal_and_file_toolsets_resolve_all_tools, which now pass both alone and with the full tests/tools/ directory (xdist ordering flake that resolved itself).
2026-04-20 23:20:33 -07:00
"""Static _PROVIDER_MODELS fallback must exist for model picker.
We only assert the provider key is present the specific model
names are data that changes with upstream releases and doesn't
belong in tests.
"""
from hermes_cli.models import _PROVIDER_MODELS
assert "xiaomi" in _PROVIDER_MODELS
test: stop testing mutable data — convert change-detectors to invariants (#13363) Catalog snapshots, config version literals, and enumeration counts are data that changes as designed. Tests that assert on those values add no behavioral coverage — they just break CI on every routine update and cost engineering time to 'fix.' Replace with invariants where one exists, delete where none does. Deleted (pure snapshots): - TestMinimaxModelCatalog (3 tests): 'MiniMax-M2.7 in models' et al - TestGeminiModelCatalog: 'gemini-2.5-pro in models', 'gemini-3.x in models' - test_browser_camofox_state::test_config_version_matches_current_schema (docstring literally said it would break on unrelated bumps) Relaxed (keep plumbing check, drop snapshot): - Xiaomi / Arcee / Kimi moonshot / Kimi coding / HuggingFace static lists: now assert 'provider exists and has >= 1 entry' instead of specific names - HuggingFace main/models.py consistency test: drop 'len >= 6' floor Dynamicized (follow source, not a literal): - 3x test_config.py migration tests: raw['_config_version'] == DEFAULT_CONFIG['_config_version'] instead of hardcoded 21 Fixed stale tests against intentional behavior changes: - test_insights::test_gateway_format_hides_cost: name matches new behavior (no dollar figures); remove contradicting '$' in text assertion - test_config::prefers_api_then_url_then_base_url: flipped per PR #9332; rename + update to base_url > url > api - test_anthropic_adapter: relax assert_called_once() (xdist-flaky) to assert called — contract is 'credential flowed through' - test_interrupt_propagation: add provider/model/_base_url to bare-agent fixture so the stale-timeout code path resolves Fixed stale integration tests against opt-in plugin gate: - transform_tool_result + transform_terminal_output: write plugins.enabled allow-list to config.yaml and reset the plugin manager singleton Source fix (real consistency invariant): - agent/model_metadata.py: add moonshotai/Kimi-K2.6 context length (262144, same as K2.5). test_model_metadata_has_context_lengths was correctly catching the gap. Policy: - AGENTS.md Testing section: new subsection 'Don't write change-detector tests' with do/don't examples. Reviewers should reject catalog-snapshot assertions in new tests. Covers every test that failed on the last completed main CI run (24703345583) except test_modal_sandbox_fixes::test_terminal_tool_present + test_terminal_and_file_toolsets_resolve_all_tools, which now pass both alone and with the full tests/tools/ directory (xdist ordering flake that resolved itself).
2026-04-20 23:20:33 -07:00
assert len(_PROVIDER_MODELS["xiaomi"]) >= 1
def test_list_agentic_models_mock(self, monkeypatch):
"""When models.dev returns Xiaomi data, list_agentic_models should return models."""
from agent import models_dev as md
fake_data = {
"xiaomi": {
"name": "Xiaomi",
"api": "https://api.xiaomimimo.com/v1",
"env": ["XIAOMI_API_KEY"],
"models": {
"mimo-v2-pro": {
"limit": {"context": 1000000},
"tool_call": True,
},
"mimo-v2-omni": {
"limit": {"context": 256000},
"tool_call": True,
},
"mimo-v2-flash": {
"limit": {"context": 256000},
"tool_call": True,
},
},
}
}
monkeypatch.setattr(md, "fetch_models_dev", lambda: fake_data)
result = md.list_agentic_models("xiaomi")
assert "mimo-v2-pro" in result
assert "mimo-v2-flash" in result
# =============================================================================
# Normalization
# =============================================================================
class TestXiaomiNormalization:
"""Model name normalization — Xiaomi is a direct provider."""
def test_vendor_prefix_mapping(self):
from hermes_cli.model_normalize import _VENDOR_PREFIXES
assert _VENDOR_PREFIXES.get("mimo") == "xiaomi"
def test_matching_prefix_strip(self):
"""xiaomi/mimo-v2-pro should normalize to mimo-v2-pro for direct API."""
from hermes_cli.model_normalize import _MATCHING_PREFIX_STRIP_PROVIDERS
assert "xiaomi" in _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")
assert result == "mimo-v2-pro"
def test_normalize_bare_name_unchanged(self):
from hermes_cli.model_normalize import normalize_model_for_provider
result = normalize_model_for_provider("mimo-v2-pro", "xiaomi")
assert result == "mimo-v2-pro"
# =============================================================================
# URL mapping
# =============================================================================
class TestXiaomiURLMapping:
"""Test URL → provider inference for Xiaomi endpoints."""
def test_url_to_provider(self):
from agent.model_metadata import _URL_TO_PROVIDER
assert _URL_TO_PROVIDER.get("api.xiaomimimo.com") == "xiaomi"
def test_provider_prefixes(self):
from agent.model_metadata import _PROVIDER_PREFIXES
assert "xiaomi" in _PROVIDER_PREFIXES
assert "mimo" in _PROVIDER_PREFIXES
assert "xiaomi-mimo" in _PROVIDER_PREFIXES
def test_infer_from_url(self):
from agent.model_metadata import _infer_provider_from_url
assert _infer_provider_from_url("https://api.xiaomimimo.com/v1") == "xiaomi"
def test_infer_from_regional_urls(self):
"""Regional token-plan endpoints should also resolve to xiaomi."""
from agent.model_metadata import _infer_provider_from_url
assert _infer_provider_from_url("https://token-plan-ams.xiaomimimo.com/v1") == "xiaomi"
assert _infer_provider_from_url("https://token-plan-cn.xiaomimimo.com/v1") == "xiaomi"
assert _infer_provider_from_url("https://token-plan-sgp.xiaomimimo.com/v1") == "xiaomi"
# =============================================================================
# providers.py
# =============================================================================
class TestXiaomiProvidersModule:
"""Test Xiaomi in the unified providers module."""
def test_overlay_exists(self):
from hermes_cli.providers import HERMES_OVERLAYS
assert "xiaomi" in HERMES_OVERLAYS
overlay = HERMES_OVERLAYS["xiaomi"]
assert overlay.transport == "openai_chat"
assert overlay.base_url_env_var == "XIAOMI_BASE_URL"
assert not overlay.is_aggregator
def test_alias_resolves(self):
from hermes_cli.providers import normalize_provider
assert normalize_provider("mimo") == "xiaomi"
assert normalize_provider("xiaomi-mimo") == "xiaomi"
def test_label(self):
from hermes_cli.providers import get_label
assert get_label("xiaomi") == "Xiaomi MiMo"
def test_get_provider(self):
pdef = None
try:
from hermes_cli.providers import get_provider
pdef = get_provider("xiaomi")
except Exception:
pass
if pdef is not None:
assert pdef.id == "xiaomi"
assert pdef.transport == "openai_chat"
# =============================================================================
# Auxiliary client
# =============================================================================
class TestXiaomiAuxiliary:
"""Xiaomi auxiliary routing: vision → omni, non-vision → user's main model, never flash."""
def test_no_flash_in_aux_models(self):
"""mimo-v2-flash must NEVER be used for automatic aux routing."""
from agent.auxiliary_client import _API_KEY_PROVIDER_AUX_MODELS
assert "xiaomi" not in _API_KEY_PROVIDER_AUX_MODELS
def test_vision_model_override(self):
"""Xiaomi vision tasks should use mimo-v2-omni (multimodal), not the main model."""
from agent.auxiliary_client import _PROVIDER_VISION_MODELS
assert "xiaomi" in _PROVIDER_VISION_MODELS
assert _PROVIDER_VISION_MODELS["xiaomi"] == "mimo-v2-omni"
# =============================================================================
# Agent init (no SyntaxError, correct api_mode)
# =============================================================================
class TestXiaomiDoctor:
"""Verify hermes doctor recognizes Xiaomi env vars."""
def test_provider_env_hints(self):
from hermes_cli.doctor import _PROVIDER_ENV_HINTS
assert "XIAOMI_API_KEY" in _PROVIDER_ENV_HINTS
class TestXiaomiAgentInit:
"""Verify the agent can be constructed with xiaomi provider without errors."""
def test_no_syntax_errors(self):
"""Importing run_agent with xiaomi should not raise."""
import importlib
importlib.import_module("run_agent")
def test_api_mode_is_chat_completions(self):
from hermes_cli.providers import HERMES_OVERLAYS, TRANSPORT_TO_API_MODE
overlay = HERMES_OVERLAYS["xiaomi"]
api_mode = TRANSPORT_TO_API_MODE[overlay.transport]
assert api_mode == "chat_completions"