diff --git a/CHANGELOG.md b/CHANGELOG.md index b6fad61a..d8a3badd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## [Unreleased] +### Fixed +- **MiniMax China provider visible in model picker** — `MINIMAX_CN_API_KEY` now maps to the `minimax-cn` provider instead of being collapsed into global `minimax`; WebUI includes a static MiniMax (China) model catalog/display label so `providers.minimax-cn: {}` can render a populated picker group. (`api/config.py`, `api/providers.py`) @franksong2702 — Closes #1236 + ## [v0.50.237] — 2026-04-29 ### Added diff --git a/api/config.py b/api/config.py index eeebb540..cb6639b3 100644 --- a/api/config.py +++ b/api/config.py @@ -536,6 +536,7 @@ _PROVIDER_DISPLAY = { "kimi-coding": "Kimi / Moonshot", "deepseek": "DeepSeek", "minimax": "MiniMax", + "minimax-cn": "MiniMax (China)", "google": "Google", "meta-llama": "Meta Llama", "huggingface": "HuggingFace", @@ -581,6 +582,8 @@ _PROVIDER_ALIASES = { "claude": "anthropic", "claude-code": "anthropic", "deep-seek": "deepseek", + "minimax-china": "minimax-cn", + "minimax_cn": "minimax-cn", "opencode": "opencode-zen", "grok": "xai", "x-ai": "xai", @@ -688,6 +691,12 @@ _PROVIDER_MODELS = { {"id": "MiniMax-M2.5-highspeed", "label": "MiniMax M2.5 Highspeed"}, {"id": "MiniMax-M2.1", "label": "MiniMax M2.1"}, ], + "minimax-cn": [ + {"id": "MiniMax-M2.7", "label": "MiniMax M2.7"}, + {"id": "MiniMax-M2.5", "label": "MiniMax M2.5"}, + {"id": "MiniMax-M2.1", "label": "MiniMax M2.1"}, + {"id": "MiniMax-M2", "label": "MiniMax M2"}, + ], # GitHub Copilot — model IDs served via the Copilot API "copilot": [ {"id": "gpt-5.5", "label": "GPT-5.5"}, @@ -1574,8 +1583,10 @@ def get_available_models() -> dict: detected_providers.add("zai") if all_env.get("KIMI_API_KEY"): detected_providers.add("kimi-coding") - if all_env.get("MINIMAX_API_KEY") or all_env.get("MINIMAX_CN_API_KEY"): + if all_env.get("MINIMAX_API_KEY"): detected_providers.add("minimax") + if all_env.get("MINIMAX_CN_API_KEY"): + detected_providers.add("minimax-cn") if all_env.get("DEEPSEEK_API_KEY"): detected_providers.add("deepseek") if all_env.get("XAI_API_KEY"): diff --git a/api/providers.py b/api/providers.py index 5563e2c3..a36a935b 100644 --- a/api/providers.py +++ b/api/providers.py @@ -39,6 +39,7 @@ _PROVIDER_ENV_VAR: dict[str, str] = { "kimi-coding": "KIMI_API_KEY", "deepseek": "DEEPSEEK_API_KEY", "minimax": "MINIMAX_API_KEY", + "minimax-cn": "MINIMAX_CN_API_KEY", "mistralai": "MISTRAL_API_KEY", "x-ai": "XAI_API_KEY", "opencode-zen": "OPENCODE_ZEN_API_KEY", diff --git a/tests/test_issue604_all_providers_model_picker.py b/tests/test_issue604_all_providers_model_picker.py index 898e05d9..e76df878 100644 --- a/tests/test_issue604_all_providers_model_picker.py +++ b/tests/test_issue604_all_providers_model_picker.py @@ -104,3 +104,6 @@ class TestProviderModelsCompleteness: def test_has_openrouter(self): # openrouter uses _FALLBACK_MODELS, not _PROVIDER_MODELS pass # intentionally no assertion + + def test_has_minimax_cn(self): + assert "minimax-cn" in _PROVIDER_MODELS_KEYS diff --git a/tests/test_minimax_provider.py b/tests/test_minimax_provider.py index c4837861..692b06cf 100644 --- a/tests/test_minimax_provider.py +++ b/tests/test_minimax_provider.py @@ -3,7 +3,7 @@ Tests for MiniMax provider support in the model/provider discovery layer. Covers: - MiniMax models appear in the fallback model list - - MINIMAX_API_KEY env var is scanned and detected from os.environ + - MINIMAX_API_KEY / MINIMAX_CN_API_KEY env vars are scanned and detected - @minimax: provider hint routing works correctly - minimax/MiniMax-M2.7 (slash format) is routed via openrouter when active provider differs """ @@ -12,6 +12,36 @@ import pytest import api.config as config +def _force_env_fallback(monkeypatch): + """Force get_available_models() down the explicit env-var fallback path.""" + import builtins + + 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") + config.cfg.clear() + config.cfg.update(cfg) + config._cfg_mtime = 0.0 + try: + return config.get_available_models() + finally: + config.cfg.clear() + config.cfg.update(old_cfg) + config._cfg_mtime = old_mtime + + @pytest.fixture(autouse=True) def _isolate_models_cache(): """Invalidate the models TTL cache before and after every test in this file.""" @@ -91,6 +121,19 @@ def test_minimax_provider_models_has_highspeed(): ) +def test_minimax_cn_provider_models_match_hermes_agent_catalog(): + """minimax-cn must have its own static catalog so an empty config provider still shows models.""" + models = config._PROVIDER_MODELS.get('minimax-cn', []) + ids = [m['id'] for m in models] + assert ids == [ + 'MiniMax-M2.7', + 'MiniMax-M2.5', + 'MiniMax-M2.1', + 'MiniMax-M2', + ] + assert config._PROVIDER_DISPLAY.get('minimax-cn') == 'MiniMax (China)' + + # ── MINIMAX_API_KEY env var detection ───────────────────────────────────────── def test_minimax_api_key_in_env_scan_tuple(): @@ -132,6 +175,55 @@ def test_minimax_detected_from_os_environ(monkeypatch): config.cfg.update(old_cfg) +def test_minimax_cn_detected_from_os_environ(monkeypatch, tmp_path): + """MINIMAX_CN_API_KEY should show MiniMax (China), not the global MiniMax provider.""" + _force_env_fallback(monkeypatch) + monkeypatch.delenv('MINIMAX_API_KEY', raising=False) + monkeypatch.setenv('MINIMAX_CN_API_KEY', 'test-cn-key-from-env') + + result = _run_available_models_with_cfg(monkeypatch, tmp_path, {'model': {}}) + groups = {g['provider_id']: g for g in result['groups']} + + assert 'minimax-cn' in groups, f"minimax-cn group missing: {groups.keys()}" + assert groups['minimax-cn']['provider'] == 'MiniMax (China)' + assert {m['id'] for m in groups['minimax-cn']['models']} == { + 'MiniMax-M2.7', + 'MiniMax-M2.5', + 'MiniMax-M2.1', + 'MiniMax-M2', + } + assert 'minimax' not in groups, ( + "MINIMAX_CN_API_KEY must not be collapsed into the global minimax provider" + ) + + +def test_minimax_cn_empty_config_provider_gets_static_models(monkeypatch, tmp_path): + """providers.minimax-cn: {} should still render a populated model group.""" + _force_env_fallback(monkeypatch) + monkeypatch.delenv('MINIMAX_API_KEY', raising=False) + monkeypatch.delenv('MINIMAX_CN_API_KEY', raising=False) + + result = _run_available_models_with_cfg( + monkeypatch, + tmp_path, + { + 'model': {'provider': 'minimax-cn', 'default': 'MiniMax-M2.7'}, + 'providers': {'minimax-cn': {}}, + }, + ) + groups = {g['provider_id']: g for g in result['groups']} + + assert 'minimax-cn' in groups, f"minimax-cn group missing: {groups.keys()}" + assert groups['minimax-cn']['models'], "minimax-cn group must not be empty" + + +def test_minimax_cn_key_can_be_managed_from_provider_settings(): + """Provider settings should use the Hermes Agent env var for minimax-cn.""" + from api.providers import _PROVIDER_ENV_VAR + + assert _PROVIDER_ENV_VAR.get('minimax-cn') == 'MINIMAX_CN_API_KEY' + + # ── Model routing ───────────────────────────────────────────────────────────── def test_provider_hint_minimax_m2_7(): diff --git a/tests/test_model_resolver.py b/tests/test_model_resolver.py index 14cd7701..4ceb1138 100644 --- a/tests/test_model_resolver.py +++ b/tests/test_model_resolver.py @@ -365,8 +365,9 @@ def test_unknown_providers_do_not_inherit_default_model(monkeypatch): """Detected providers without their own model catalog must not be filled with the global default_model placeholder. - Regression guard for the bug where Alibaba / Minimax-Cn ended up showing - gpt-5.4-mini even though those providers do not serve it. + Regression guard for the bug where unknown providers ended up showing + gpt-5.4-mini even though those providers do not serve it. Minimax-Cn is + now known and should show its own catalog instead. """ import sys, types @@ -392,12 +393,12 @@ def test_unknown_providers_do_not_inherit_default_model(monkeypatch): assert 'Alibaba' not in groups, ( f"Alibaba should not inherit the default model placeholder: {groups}" ) - assert 'Minimax-Cn' not in groups, ( - f"Minimax-Cn should not inherit the default model placeholder: {groups}" + assert 'MiniMax (China)' in groups, ( + f"Minimax-Cn should render its own static catalog: {groups}" ) assert not any( norm(mid) == 'gpt-5.4-mini' - for mid in groups.get('Alibaba', []) + groups.get('Minimax-Cn', []) + for mid in groups.get('Alibaba', []) + groups.get('MiniMax (China)', []) ), ( f"Unknown provider groups still inherited the default model: {groups}" )