diff --git a/CHANGELOG.md b/CHANGELOG.md index c75a2334..f8cd873f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Hermes Web UI -- Changelog +## [Unreleased] + +### Fixed + +- **fix(providers): detect Xiaomi MiMo from `XIAOMI_API_KEY`** ([#2025](https://github.com/nesquena/hermes-webui/issues/2025)). WebUI now treats Xiaomi like the other API-key providers: exported or `.env`-stored `XIAOMI_API_KEY` enables the Xiaomi model group, Settings provider-key detection, and onboarding help text without requiring a manual provider config entry. + ## [v0.51.39] — 2026-05-10 — Release O (4-PR contributor batch — Railway docker fix + Stop-button race + provider resolver + live context tracking) ### Fixed diff --git a/README.md b/README.md index 5fa41870..951ec728 100644 --- a/README.md +++ b/README.md @@ -375,7 +375,7 @@ across 100+ test files. ### Chat and agent - Streaming responses via SSE (tokens appear as they are generated) -- Multi-provider model support -- any Hermes API provider (OpenAI, Anthropic, Google, DeepSeek, Nous Portal, OpenRouter, MiniMax, Z.AI); dynamic model dropdown populated from configured keys +- Multi-provider model support -- any Hermes API provider (OpenAI, Anthropic, Google, DeepSeek, Nous Portal, OpenRouter, MiniMax, Xiaomi MiMo, Z.AI); dynamic model dropdown populated from configured keys - Send a message while one is processing -- it queues automatically - Edit any past user message inline and regenerate from that point - Retry the last assistant response with one click diff --git a/api/config.py b/api/config.py index 5a592dea..906d1fcd 100644 --- a/api/config.py +++ b/api/config.py @@ -2760,6 +2760,7 @@ def get_available_models() -> dict: "GLM_API_KEY", "KIMI_API_KEY", "DEEPSEEK_API_KEY", + "XIAOMI_API_KEY", "OPENCODE_ZEN_API_KEY", "OPENCODE_GO_API_KEY", "MINIMAX_API_KEY", @@ -2795,6 +2796,8 @@ def get_available_models() -> dict: detected_providers.add("minimax-cn") if all_env.get("DEEPSEEK_API_KEY"): detected_providers.add("deepseek") + if all_env.get("XIAOMI_API_KEY"): + detected_providers.add("xiaomi") if all_env.get("XAI_API_KEY"): detected_providers.add("x-ai") if all_env.get("MISTRAL_API_KEY"): diff --git a/api/onboarding.py b/api/onboarding.py index 84da3ca6..806e4856 100644 --- a/api/onboarding.py +++ b/api/onboarding.py @@ -139,6 +139,15 @@ _SUPPORTED_PROVIDER_SETUPS = { "models": list(_PROVIDER_MODELS.get("deepseek", [])), "category": "specialized", }, + "xiaomi": { + "label": "Xiaomi MiMo", + "env_var": "XIAOMI_API_KEY", + "default_model": "mimo-v2.5-pro", + "default_base_url": "https://api.xiaomimimo.com/v1", + "requires_base_url": False, + "models": list(_PROVIDER_MODELS.get("xiaomi", [])), + "category": "specialized", + }, "zai": { "label": "Z.AI / GLM (智谱)", "env_var": "GLM_API_KEY", diff --git a/api/providers.py b/api/providers.py index 495325f8..07cfe2f3 100644 --- a/api/providers.py +++ b/api/providers.py @@ -102,6 +102,7 @@ _PROVIDER_ENV_VAR: dict[str, str] = { "minimax-cn": "MINIMAX_CN_API_KEY", "mistralai": "MISTRAL_API_KEY", "x-ai": "XAI_API_KEY", + "xiaomi": "XIAOMI_API_KEY", "opencode-zen": "OPENCODE_ZEN_API_KEY", "opencode-go": "OPENCODE_GO_API_KEY", # NOTE: bare "ollama" (local) deliberately omitted — local Ollama is keyless diff --git a/tests/conftest.py b/tests/conftest.py index b1a09580..f8b6eca5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -291,7 +291,7 @@ def test_server(): for _k in list(env): if any(_k.startswith(p) for p in ( 'OPENROUTER_API_KEY', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', - 'GOOGLE_API_KEY', 'DEEPSEEK_API_KEY', + 'GOOGLE_API_KEY', 'DEEPSEEK_API_KEY', 'XIAOMI_API_KEY', )): del env[_k] env.update({ diff --git a/tests/test_issue1094_provider_bugs.py b/tests/test_issue1094_provider_bugs.py index edd17e85..6c37799e 100644 --- a/tests/test_issue1094_provider_bugs.py +++ b/tests/test_issue1094_provider_bugs.py @@ -85,7 +85,7 @@ def _setup_clean_config(monkeypatch, tmp_path): "OPENROUTER_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GOOGLE_API_KEY", "GEMINI_API_KEY", "GLM_API_KEY", "KIMI_API_KEY", "DEEPSEEK_API_KEY", "MINIMAX_API_KEY", - "MISTRAL_API_KEY", "XAI_API_KEY", "OLLAMA_API_KEY", + "XIAOMI_API_KEY", "MISTRAL_API_KEY", "XAI_API_KEY", "OLLAMA_API_KEY", "OPENCODE_ZEN_API_KEY", "OPENCODE_GO_API_KEY", ] for var in _provider_env_vars: diff --git a/tests/test_issue1240_generic_cli_catalog_sync.py b/tests/test_issue1240_generic_cli_catalog_sync.py index d0ba5dd0..0f59de52 100644 --- a/tests/test_issue1240_generic_cli_catalog_sync.py +++ b/tests/test_issue1240_generic_cli_catalog_sync.py @@ -22,6 +22,7 @@ _PROVIDER_ENV_VARS = ( "GLM_API_KEY", "KIMI_API_KEY", "DEEPSEEK_API_KEY", + "XIAOMI_API_KEY", "OPENCODE_ZEN_API_KEY", "OPENCODE_GO_API_KEY", "MINIMAX_API_KEY", diff --git a/tests/test_issue1420_lmstudio_provider_env_var.py b/tests/test_issue1420_lmstudio_provider_env_var.py index 1143da80..6db74a2a 100644 --- a/tests/test_issue1420_lmstudio_provider_env_var.py +++ b/tests/test_issue1420_lmstudio_provider_env_var.py @@ -243,7 +243,7 @@ class TestIssue1420LMStudioProviderEnvVar: "OPENAI_API_KEY", "OPENROUTER_API_KEY", "ANTHROPIC_API_KEY", "GH_TOKEN", "GITHUB_TOKEN", "OLLAMA_API_KEY", "GOOGLE_API_KEY", "GEMINI_API_KEY", "DEEPSEEK_API_KEY", "MINIMAX_API_KEY", - "MINIMAX_CN_API_KEY", "MISTRAL_API_KEY", "XAI_API_KEY", + "MINIMAX_CN_API_KEY", "XIAOMI_API_KEY", "MISTRAL_API_KEY", "XAI_API_KEY", "GLM_API_KEY", "KIMI_API_KEY", "OPENCODE_ZEN_API_KEY", "OPENCODE_GO_API_KEY", "NVIDIA_API_KEY", "LMSTUDIO_API_KEY", ): diff --git a/tests/test_issue1538_nous_live_catalog.py b/tests/test_issue1538_nous_live_catalog.py index 4a9ddb03..9e718a9a 100644 --- a/tests/test_issue1538_nous_live_catalog.py +++ b/tests/test_issue1538_nous_live_catalog.py @@ -126,6 +126,7 @@ def _scrub_provider_env(monkeypatch): "OLLAMA_CLOUD_API_KEY", "OLLAMA_API_KEY", "GLM_API_KEY", "KIMI_API_KEY", "MOONSHOT_API_KEY", "MINIMAX_API_KEY", "MINIMAX_CN_API_KEY", + "XIAOMI_API_KEY", "OPENCODE_ZEN_API_KEY", "OPENCODE_GO_API_KEY", "NOUS_API_KEY", "NVIDIA_API_KEY", "LM_API_KEY", "LMSTUDIO_API_KEY", ): diff --git a/tests/test_issue1567_nous_picker_capacity_and_symmetry.py b/tests/test_issue1567_nous_picker_capacity_and_symmetry.py index 75a09144..c04f28f1 100644 --- a/tests/test_issue1567_nous_picker_capacity_and_symmetry.py +++ b/tests/test_issue1567_nous_picker_capacity_and_symmetry.py @@ -127,6 +127,7 @@ def _scrub_provider_env(monkeypatch): "OLLAMA_CLOUD_API_KEY", "OLLAMA_API_KEY", "GLM_API_KEY", "KIMI_API_KEY", "MOONSHOT_API_KEY", "MINIMAX_API_KEY", "MINIMAX_CN_API_KEY", + "XIAOMI_API_KEY", "OPENCODE_ZEN_API_KEY", "OPENCODE_GO_API_KEY", "NOUS_API_KEY", "NVIDIA_API_KEY", "LM_API_KEY", "LMSTUDIO_API_KEY", ): diff --git a/tests/test_issue1568_duplicate_provider_groups.py b/tests/test_issue1568_duplicate_provider_groups.py index 70e5938a..911ba644 100644 --- a/tests/test_issue1568_duplicate_provider_groups.py +++ b/tests/test_issue1568_duplicate_provider_groups.py @@ -85,6 +85,7 @@ def _scrub_provider_env(monkeypatch): "OLLAMA_CLOUD_API_KEY", "OLLAMA_API_KEY", "GLM_API_KEY", "KIMI_API_KEY", "MOONSHOT_API_KEY", "MINIMAX_API_KEY", "MINIMAX_CN_API_KEY", + "XIAOMI_API_KEY", "OPENCODE_ZEN_API_KEY", "OPENCODE_GO_API_KEY", "NOUS_API_KEY", "NVIDIA_API_KEY", "LM_API_KEY", "LMSTUDIO_API_KEY", ): diff --git a/tests/test_issue2025_xiaomi_env_key.py b/tests/test_issue2025_xiaomi_env_key.py new file mode 100644 index 00000000..4c3cbebe --- /dev/null +++ b/tests/test_issue2025_xiaomi_env_key.py @@ -0,0 +1,68 @@ +"""Regression coverage for #2025: Xiaomi MiMo should honor XIAOMI_API_KEY.""" + +from __future__ import annotations + +import builtins + +import api.config as config +import api.onboarding as onboarding +import api.providers as providers + + +def _force_env_fallback(monkeypatch): + """Force get_available_models() down its explicit env-var fallback path.""" + real_import = builtins.__import__ + + def fake_import(name, globals=None, locals=None, fromlist=(), level=0): + if name in ("hermes_cli.models", "hermes_cli.auth"): + raise ImportError(name) + return real_import(name, globals, locals, fromlist, level) + + monkeypatch.setattr(builtins, "__import__", fake_import) + + +def _run_available_models_with_cfg(monkeypatch, tmp_path, cfg): + old_cfg = dict(config.cfg) + old_mtime = config._cfg_mtime + monkeypatch.setattr(config, "_models_cache_path", tmp_path / "models_cache.json") + monkeypatch.setattr(config, "_get_config_path", lambda: tmp_path / "missing-config.yaml") + monkeypatch.setattr("api.profiles.get_active_hermes_home", lambda: tmp_path, raising=False) + config.cfg.clear() + config.cfg.update(cfg) + config._cfg_mtime = 0.0 + config.invalidate_models_cache() + try: + return config.get_available_models() + finally: + config.cfg.clear() + config.cfg.update(old_cfg) + config._cfg_mtime = old_mtime + config.invalidate_models_cache() + + +def test_xiaomi_api_key_env_var_detects_model_group(monkeypatch, tmp_path): + _force_env_fallback(monkeypatch) + monkeypatch.setenv("XIAOMI_API_KEY", "test-xiaomi-key") + + result = _run_available_models_with_cfg(monkeypatch, tmp_path, {"model": {}}) + groups = {group["provider_id"]: group for group in result["groups"]} + + assert "xiaomi" in groups + assert groups["xiaomi"]["provider"] == "Xiaomi" + assert "mimo-v2.5-pro" in {model["id"] for model in groups["xiaomi"]["models"]} + + +def test_xiaomi_provider_settings_detects_env_key(monkeypatch, tmp_path): + monkeypatch.setattr(providers, "_get_hermes_home", lambda: tmp_path) + monkeypatch.setenv("XIAOMI_API_KEY", "test-xiaomi-key") + + assert providers._PROVIDER_ENV_VAR["xiaomi"] == "XIAOMI_API_KEY" + assert providers._provider_has_key("xiaomi") is True + + +def test_onboarding_lists_xiaomi_api_key_help(): + setup = onboarding._SUPPORTED_PROVIDER_SETUPS["xiaomi"] + + assert setup["env_var"] == "XIAOMI_API_KEY" + assert setup["default_base_url"] == "https://api.xiaomimimo.com/v1" + assert {model["id"] for model in setup["models"]} >= {"mimo-v2.5-pro"}