Fix Xiaomi API key env detection
This commit is contained in:
parent
b997067ae8
commit
128e734df4
13 changed files with 95 additions and 4 deletions
|
|
@ -1,5 +1,11 @@
|
||||||
# Hermes Web UI -- Changelog
|
# 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)
|
## [v0.51.39] — 2026-05-10 — Release O (4-PR contributor batch — Railway docker fix + Stop-button race + provider resolver + live context tracking)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
|
||||||
|
|
@ -375,7 +375,7 @@ across 100+ test files.
|
||||||
|
|
||||||
### Chat and agent
|
### Chat and agent
|
||||||
- Streaming responses via SSE (tokens appear as they are generated)
|
- 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
|
- Send a message while one is processing -- it queues automatically
|
||||||
- Edit any past user message inline and regenerate from that point
|
- Edit any past user message inline and regenerate from that point
|
||||||
- Retry the last assistant response with one click
|
- Retry the last assistant response with one click
|
||||||
|
|
|
||||||
|
|
@ -2760,6 +2760,7 @@ def get_available_models() -> dict:
|
||||||
"GLM_API_KEY",
|
"GLM_API_KEY",
|
||||||
"KIMI_API_KEY",
|
"KIMI_API_KEY",
|
||||||
"DEEPSEEK_API_KEY",
|
"DEEPSEEK_API_KEY",
|
||||||
|
"XIAOMI_API_KEY",
|
||||||
"OPENCODE_ZEN_API_KEY",
|
"OPENCODE_ZEN_API_KEY",
|
||||||
"OPENCODE_GO_API_KEY",
|
"OPENCODE_GO_API_KEY",
|
||||||
"MINIMAX_API_KEY",
|
"MINIMAX_API_KEY",
|
||||||
|
|
@ -2795,6 +2796,8 @@ def get_available_models() -> dict:
|
||||||
detected_providers.add("minimax-cn")
|
detected_providers.add("minimax-cn")
|
||||||
if all_env.get("DEEPSEEK_API_KEY"):
|
if all_env.get("DEEPSEEK_API_KEY"):
|
||||||
detected_providers.add("deepseek")
|
detected_providers.add("deepseek")
|
||||||
|
if all_env.get("XIAOMI_API_KEY"):
|
||||||
|
detected_providers.add("xiaomi")
|
||||||
if all_env.get("XAI_API_KEY"):
|
if all_env.get("XAI_API_KEY"):
|
||||||
detected_providers.add("x-ai")
|
detected_providers.add("x-ai")
|
||||||
if all_env.get("MISTRAL_API_KEY"):
|
if all_env.get("MISTRAL_API_KEY"):
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,15 @@ _SUPPORTED_PROVIDER_SETUPS = {
|
||||||
"models": list(_PROVIDER_MODELS.get("deepseek", [])),
|
"models": list(_PROVIDER_MODELS.get("deepseek", [])),
|
||||||
"category": "specialized",
|
"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": {
|
"zai": {
|
||||||
"label": "Z.AI / GLM (智谱)",
|
"label": "Z.AI / GLM (智谱)",
|
||||||
"env_var": "GLM_API_KEY",
|
"env_var": "GLM_API_KEY",
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,7 @@ _PROVIDER_ENV_VAR: dict[str, str] = {
|
||||||
"minimax-cn": "MINIMAX_CN_API_KEY",
|
"minimax-cn": "MINIMAX_CN_API_KEY",
|
||||||
"mistralai": "MISTRAL_API_KEY",
|
"mistralai": "MISTRAL_API_KEY",
|
||||||
"x-ai": "XAI_API_KEY",
|
"x-ai": "XAI_API_KEY",
|
||||||
|
"xiaomi": "XIAOMI_API_KEY",
|
||||||
"opencode-zen": "OPENCODE_ZEN_API_KEY",
|
"opencode-zen": "OPENCODE_ZEN_API_KEY",
|
||||||
"opencode-go": "OPENCODE_GO_API_KEY",
|
"opencode-go": "OPENCODE_GO_API_KEY",
|
||||||
# NOTE: bare "ollama" (local) deliberately omitted — local Ollama is keyless
|
# NOTE: bare "ollama" (local) deliberately omitted — local Ollama is keyless
|
||||||
|
|
|
||||||
|
|
@ -291,7 +291,7 @@ def test_server():
|
||||||
for _k in list(env):
|
for _k in list(env):
|
||||||
if any(_k.startswith(p) for p in (
|
if any(_k.startswith(p) for p in (
|
||||||
'OPENROUTER_API_KEY', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY',
|
'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]
|
del env[_k]
|
||||||
env.update({
|
env.update({
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ def _setup_clean_config(monkeypatch, tmp_path):
|
||||||
"OPENROUTER_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY",
|
"OPENROUTER_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY",
|
||||||
"GOOGLE_API_KEY", "GEMINI_API_KEY", "GLM_API_KEY",
|
"GOOGLE_API_KEY", "GEMINI_API_KEY", "GLM_API_KEY",
|
||||||
"KIMI_API_KEY", "DEEPSEEK_API_KEY", "MINIMAX_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",
|
"OPENCODE_ZEN_API_KEY", "OPENCODE_GO_API_KEY",
|
||||||
]
|
]
|
||||||
for var in _provider_env_vars:
|
for var in _provider_env_vars:
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ _PROVIDER_ENV_VARS = (
|
||||||
"GLM_API_KEY",
|
"GLM_API_KEY",
|
||||||
"KIMI_API_KEY",
|
"KIMI_API_KEY",
|
||||||
"DEEPSEEK_API_KEY",
|
"DEEPSEEK_API_KEY",
|
||||||
|
"XIAOMI_API_KEY",
|
||||||
"OPENCODE_ZEN_API_KEY",
|
"OPENCODE_ZEN_API_KEY",
|
||||||
"OPENCODE_GO_API_KEY",
|
"OPENCODE_GO_API_KEY",
|
||||||
"MINIMAX_API_KEY",
|
"MINIMAX_API_KEY",
|
||||||
|
|
|
||||||
|
|
@ -243,7 +243,7 @@ class TestIssue1420LMStudioProviderEnvVar:
|
||||||
"OPENAI_API_KEY", "OPENROUTER_API_KEY", "ANTHROPIC_API_KEY",
|
"OPENAI_API_KEY", "OPENROUTER_API_KEY", "ANTHROPIC_API_KEY",
|
||||||
"GH_TOKEN", "GITHUB_TOKEN", "OLLAMA_API_KEY", "GOOGLE_API_KEY",
|
"GH_TOKEN", "GITHUB_TOKEN", "OLLAMA_API_KEY", "GOOGLE_API_KEY",
|
||||||
"GEMINI_API_KEY", "DEEPSEEK_API_KEY", "MINIMAX_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",
|
"GLM_API_KEY", "KIMI_API_KEY", "OPENCODE_ZEN_API_KEY",
|
||||||
"OPENCODE_GO_API_KEY", "NVIDIA_API_KEY", "LMSTUDIO_API_KEY",
|
"OPENCODE_GO_API_KEY", "NVIDIA_API_KEY", "LMSTUDIO_API_KEY",
|
||||||
):
|
):
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,7 @@ def _scrub_provider_env(monkeypatch):
|
||||||
"OLLAMA_CLOUD_API_KEY", "OLLAMA_API_KEY",
|
"OLLAMA_CLOUD_API_KEY", "OLLAMA_API_KEY",
|
||||||
"GLM_API_KEY", "KIMI_API_KEY", "MOONSHOT_API_KEY",
|
"GLM_API_KEY", "KIMI_API_KEY", "MOONSHOT_API_KEY",
|
||||||
"MINIMAX_API_KEY", "MINIMAX_CN_API_KEY",
|
"MINIMAX_API_KEY", "MINIMAX_CN_API_KEY",
|
||||||
|
"XIAOMI_API_KEY",
|
||||||
"OPENCODE_ZEN_API_KEY", "OPENCODE_GO_API_KEY",
|
"OPENCODE_ZEN_API_KEY", "OPENCODE_GO_API_KEY",
|
||||||
"NOUS_API_KEY", "NVIDIA_API_KEY", "LM_API_KEY", "LMSTUDIO_API_KEY",
|
"NOUS_API_KEY", "NVIDIA_API_KEY", "LM_API_KEY", "LMSTUDIO_API_KEY",
|
||||||
):
|
):
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,7 @@ def _scrub_provider_env(monkeypatch):
|
||||||
"OLLAMA_CLOUD_API_KEY", "OLLAMA_API_KEY",
|
"OLLAMA_CLOUD_API_KEY", "OLLAMA_API_KEY",
|
||||||
"GLM_API_KEY", "KIMI_API_KEY", "MOONSHOT_API_KEY",
|
"GLM_API_KEY", "KIMI_API_KEY", "MOONSHOT_API_KEY",
|
||||||
"MINIMAX_API_KEY", "MINIMAX_CN_API_KEY",
|
"MINIMAX_API_KEY", "MINIMAX_CN_API_KEY",
|
||||||
|
"XIAOMI_API_KEY",
|
||||||
"OPENCODE_ZEN_API_KEY", "OPENCODE_GO_API_KEY",
|
"OPENCODE_ZEN_API_KEY", "OPENCODE_GO_API_KEY",
|
||||||
"NOUS_API_KEY", "NVIDIA_API_KEY", "LM_API_KEY", "LMSTUDIO_API_KEY",
|
"NOUS_API_KEY", "NVIDIA_API_KEY", "LM_API_KEY", "LMSTUDIO_API_KEY",
|
||||||
):
|
):
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,7 @@ def _scrub_provider_env(monkeypatch):
|
||||||
"OLLAMA_CLOUD_API_KEY", "OLLAMA_API_KEY",
|
"OLLAMA_CLOUD_API_KEY", "OLLAMA_API_KEY",
|
||||||
"GLM_API_KEY", "KIMI_API_KEY", "MOONSHOT_API_KEY",
|
"GLM_API_KEY", "KIMI_API_KEY", "MOONSHOT_API_KEY",
|
||||||
"MINIMAX_API_KEY", "MINIMAX_CN_API_KEY",
|
"MINIMAX_API_KEY", "MINIMAX_CN_API_KEY",
|
||||||
|
"XIAOMI_API_KEY",
|
||||||
"OPENCODE_ZEN_API_KEY", "OPENCODE_GO_API_KEY",
|
"OPENCODE_ZEN_API_KEY", "OPENCODE_GO_API_KEY",
|
||||||
"NOUS_API_KEY", "NVIDIA_API_KEY", "LM_API_KEY", "LMSTUDIO_API_KEY",
|
"NOUS_API_KEY", "NVIDIA_API_KEY", "LM_API_KEY", "LMSTUDIO_API_KEY",
|
||||||
):
|
):
|
||||||
|
|
|
||||||
68
tests/test_issue2025_xiaomi_env_key.py
Normal file
68
tests/test_issue2025_xiaomi_env_key.py
Normal file
|
|
@ -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"}
|
||||||
Loading…
Reference in a new issue