test_gmi_provider.py
1 """Focused tests for GMI Cloud first-class provider wiring.""" 2 3 from __future__ import annotations 4 5 import contextlib 6 import io 7 import sys 8 import types 9 from argparse import Namespace 10 from unittest.mock import patch 11 12 import pytest 13 14 if "dotenv" not in sys.modules: 15 fake_dotenv = types.ModuleType("dotenv") 16 fake_dotenv.load_dotenv = lambda *args, **kwargs: None 17 sys.modules["dotenv"] = fake_dotenv 18 19 from hermes_cli.auth import resolve_provider 20 from hermes_cli.config import load_config 21 from hermes_cli.models import ( 22 CANONICAL_PROVIDERS, 23 _PROVIDER_LABELS, 24 _PROVIDER_MODELS, 25 normalize_provider, 26 provider_model_ids, 27 ) 28 from agent.auxiliary_client import resolve_provider_client 29 from agent.model_metadata import get_model_context_length 30 31 32 @pytest.fixture(autouse=True) 33 def _clear_provider_env(monkeypatch): 34 for key in ( 35 "OPENROUTER_API_KEY", 36 "OPENAI_API_KEY", 37 "ANTHROPIC_API_KEY", 38 "GOOGLE_API_KEY", 39 "GLM_API_KEY", 40 "KIMI_API_KEY", 41 "MINIMAX_API_KEY", 42 "GMI_API_KEY", 43 "GMI_BASE_URL", 44 ): 45 monkeypatch.delenv(key, raising=False) 46 47 48 class TestGmiAliases: 49 @pytest.mark.parametrize("alias", ["gmi", "gmi-cloud", "gmicloud"]) 50 def test_alias_resolves(self, alias, monkeypatch): 51 monkeypatch.setenv("GMI_API_KEY", "gmi-test-key") 52 assert resolve_provider(alias) == "gmi" 53 54 def test_models_normalize_provider(self): 55 assert normalize_provider("gmi-cloud") == "gmi" 56 assert normalize_provider("gmicloud") == "gmi" 57 58 def test_providers_normalize_provider(self): 59 from hermes_cli.providers import normalize_provider as normalize_provider_in_providers 60 61 assert normalize_provider_in_providers("gmi-cloud") == "gmi" 62 assert normalize_provider_in_providers("gmicloud") == "gmi" 63 64 65 class TestGmiConfigRegistry: 66 def test_optional_env_vars_include_gmi(self): 67 from hermes_cli.config import OPTIONAL_ENV_VARS 68 69 assert "GMI_API_KEY" in OPTIONAL_ENV_VARS 70 assert OPTIONAL_ENV_VARS["GMI_API_KEY"]["category"] == "provider" 71 assert OPTIONAL_ENV_VARS["GMI_API_KEY"]["password"] is True 72 assert OPTIONAL_ENV_VARS["GMI_API_KEY"]["url"] == "https://www.gmicloud.ai/" 73 74 assert "GMI_BASE_URL" in OPTIONAL_ENV_VARS 75 assert OPTIONAL_ENV_VARS["GMI_BASE_URL"]["category"] == "provider" 76 assert OPTIONAL_ENV_VARS["GMI_BASE_URL"]["password"] is False 77 # ENV_VARS_BY_VERSION entries are not needed for providers added after 78 # _config_version 22 (the current baseline) — users discover GMI via 79 # hermes model, not via upgrade prompts. 80 81 82 class TestGmiModelCatalog: 83 def test_static_model_fallback_exists(self): 84 assert "gmi" in _PROVIDER_MODELS 85 models = _PROVIDER_MODELS["gmi"] 86 assert "zai-org/GLM-5.1-FP8" in models 87 assert "deepseek-ai/DeepSeek-V3.2" in models 88 assert "moonshotai/Kimi-K2.5" in models 89 assert "anthropic/claude-sonnet-4.6" in models 90 91 def test_canonical_provider_entry(self): 92 slugs = [p.slug for p in CANONICAL_PROVIDERS] 93 assert "gmi" in slugs 94 95 def test_provider_model_ids_prefers_live_api(self, monkeypatch): 96 monkeypatch.setattr( 97 "hermes_cli.auth.resolve_api_key_provider_credentials", 98 lambda provider_id: { 99 "provider": provider_id, 100 "api_key": "gmi-live-key", 101 "base_url": "https://api.gmi-serving.com/v1", 102 "source": "GMI_API_KEY", 103 }, 104 ) 105 monkeypatch.setattr( 106 "hermes_cli.models.fetch_api_models", 107 lambda api_key, base_url: [ 108 "openai/gpt-5.4-mini", 109 "zai-org/GLM-5.1-FP8", 110 ], 111 ) 112 113 assert provider_model_ids("gmi") == [ 114 "openai/gpt-5.4-mini", 115 "zai-org/GLM-5.1-FP8", 116 ] 117 118 def test_provider_model_ids_falls_back_to_static_models(self, monkeypatch): 119 monkeypatch.setattr( 120 "hermes_cli.auth.resolve_api_key_provider_credentials", 121 lambda provider_id: { 122 "provider": provider_id, 123 "api_key": "gmi-live-key", 124 "base_url": "https://api.gmi-serving.com/v1", 125 "source": "GMI_API_KEY", 126 }, 127 ) 128 monkeypatch.setattr("hermes_cli.models.fetch_api_models", lambda api_key, base_url: None) 129 130 assert provider_model_ids("gmi") == list(_PROVIDER_MODELS["gmi"]) 131 132 133 class TestGmiProvidersModule: 134 def test_overlay_exists(self): 135 from hermes_cli.providers import HERMES_OVERLAYS 136 137 assert "gmi" in HERMES_OVERLAYS 138 overlay = HERMES_OVERLAYS["gmi"] 139 assert overlay.transport == "openai_chat" 140 assert overlay.extra_env_vars == ("GMI_API_KEY",) 141 assert overlay.base_url_override == "https://api.gmi-serving.com/v1" 142 assert overlay.base_url_env_var == "GMI_BASE_URL" 143 assert not overlay.is_aggregator 144 145 def test_provider_label(self): 146 assert _PROVIDER_LABELS["gmi"] == "GMI Cloud" 147 148 149 class TestGmiDoctor: 150 def test_provider_env_hints_include_gmi(self): 151 from hermes_cli.doctor import _PROVIDER_ENV_HINTS 152 153 assert "GMI_API_KEY" in _PROVIDER_ENV_HINTS 154 155 def test_run_doctor_checks_gmi_models_endpoint(self, monkeypatch, tmp_path): 156 from hermes_cli import doctor as doctor_mod 157 158 home = tmp_path / ".hermes" 159 home.mkdir(parents=True, exist_ok=True) 160 (home / "config.yaml").write_text("memory: {}\n", encoding="utf-8") 161 (home / ".env").write_text("GMI_API_KEY=***\n", encoding="utf-8") 162 project = tmp_path / "project" 163 project.mkdir(exist_ok=True) 164 165 monkeypatch.setattr(doctor_mod, "HERMES_HOME", home) 166 monkeypatch.setattr(doctor_mod, "PROJECT_ROOT", project) 167 monkeypatch.setattr(doctor_mod, "_DHH", str(home)) 168 monkeypatch.setenv("GMI_API_KEY", "gmi-test-key") 169 170 for env_name in ( 171 "OPENROUTER_API_KEY", 172 "OPENAI_API_KEY", 173 "ANTHROPIC_API_KEY", 174 "ANTHROPIC_TOKEN", 175 "GLM_API_KEY", 176 "ZAI_API_KEY", 177 "Z_AI_API_KEY", 178 "KIMI_API_KEY", 179 "KIMI_CN_API_KEY", 180 "ARCEEAI_API_KEY", 181 "DEEPSEEK_API_KEY", 182 "HF_TOKEN", 183 "DASHSCOPE_API_KEY", 184 "MINIMAX_API_KEY", 185 "MINIMAX_CN_API_KEY", 186 "AI_GATEWAY_API_KEY", 187 "KILOCODE_API_KEY", 188 "OPENCODE_ZEN_API_KEY", 189 "OPENCODE_GO_API_KEY", 190 "XIAOMI_API_KEY", 191 ): 192 monkeypatch.delenv(env_name, raising=False) 193 194 fake_model_tools = types.SimpleNamespace( 195 check_tool_availability=lambda *a, **kw: ([], []), 196 TOOLSET_REQUIREMENTS={}, 197 ) 198 monkeypatch.setitem(sys.modules, "model_tools", fake_model_tools) 199 200 try: 201 from hermes_cli import auth as _auth_mod 202 203 monkeypatch.setattr(_auth_mod, "get_nous_auth_status", lambda: {}) 204 monkeypatch.setattr(_auth_mod, "get_codex_auth_status", lambda: {}) 205 except Exception: 206 pass 207 208 calls = [] 209 210 def fake_get(url, headers=None, timeout=None): 211 calls.append((url, headers, timeout)) 212 return types.SimpleNamespace(status_code=200) 213 214 import httpx 215 216 monkeypatch.setattr(httpx, "get", fake_get) 217 218 buf = io.StringIO() 219 with contextlib.redirect_stdout(buf): 220 doctor_mod.run_doctor(Namespace(fix=False)) 221 out = buf.getvalue() 222 223 assert "API key or custom endpoint configured" in out 224 assert "GMI Cloud" in out 225 assert any(url == "https://api.gmi-serving.com/v1/models" for url, _, _ in calls) 226 227 228 class TestGmiModelMetadata: 229 def test_url_to_provider(self): 230 from agent.model_metadata import _URL_TO_PROVIDER 231 232 assert _URL_TO_PROVIDER.get("api.gmi-serving.com") == "gmi" 233 234 def test_provider_prefixes(self): 235 from agent.model_metadata import _PROVIDER_PREFIXES 236 237 assert "gmi" in _PROVIDER_PREFIXES 238 assert "gmi-cloud" in _PROVIDER_PREFIXES 239 assert "gmicloud" in _PROVIDER_PREFIXES 240 241 def test_infer_from_url(self): 242 from agent.model_metadata import _infer_provider_from_url 243 244 assert _infer_provider_from_url("https://api.gmi-serving.com/v1") == "gmi" 245 246 def test_known_gmi_endpoint_still_uses_endpoint_metadata(self): 247 with patch( 248 "agent.model_metadata.get_cached_context_length", 249 return_value=None, 250 ), patch( 251 "agent.model_metadata.fetch_endpoint_model_metadata", 252 return_value={"anthropic/claude-opus-4.6": {"context_length": 409600}}, 253 ), patch( 254 "agent.models_dev.lookup_models_dev_context", 255 return_value=None, 256 ), patch( 257 "agent.model_metadata.fetch_model_metadata", 258 return_value={}, 259 ): 260 result = get_model_context_length( 261 "anthropic/claude-opus-4.6", 262 base_url="https://api.gmi-serving.com/v1", 263 api_key="gmi-test-key", 264 provider="custom", 265 ) 266 267 assert result == 409600 268 269 270 class TestGmiAuxiliary: 271 def test_aux_default_model(self): 272 from agent.auxiliary_client import _API_KEY_PROVIDER_AUX_MODELS 273 274 assert _API_KEY_PROVIDER_AUX_MODELS["gmi"] == "google/gemini-3.1-flash-lite-preview" 275 276 def test_resolve_provider_client_uses_gmi_aux_default(self, monkeypatch): 277 monkeypatch.setenv("GMI_API_KEY", "gmi-test-key") 278 279 with patch("agent.auxiliary_client.OpenAI") as mock_openai: 280 mock_openai.return_value = object() 281 client, model = resolve_provider_client("gmi") 282 283 assert client is not None 284 assert model == "google/gemini-3.1-flash-lite-preview" 285 assert mock_openai.call_args.kwargs["api_key"] == "gmi-test-key" 286 assert mock_openai.call_args.kwargs["base_url"] == "https://api.gmi-serving.com/v1" 287 288 def test_resolve_provider_client_accepts_gmi_alias(self, monkeypatch): 289 monkeypatch.setenv("GMI_API_KEY", "gmi-test-key") 290 291 with patch("agent.auxiliary_client.OpenAI") as mock_openai: 292 mock_openai.return_value = object() 293 client, model = resolve_provider_client("gmi-cloud") 294 295 assert client is not None 296 assert model == "google/gemini-3.1-flash-lite-preview" 297 298 299 class TestGmiMainFlow: 300 def test_chat_parser_accepts_gmi_provider(self, monkeypatch): 301 recorded: dict[str, str] = {} 302 303 monkeypatch.setattr("hermes_cli.config.get_container_exec_info", lambda: None) 304 monkeypatch.setattr( 305 "hermes_cli.main.cmd_chat", 306 lambda args: recorded.setdefault("provider", args.provider), 307 ) 308 monkeypatch.setattr(sys, "argv", ["hermes", "chat", "--provider", "gmi"]) 309 310 from hermes_cli.main import main 311 312 main() 313 314 assert recorded["provider"] == "gmi" 315 316 def test_select_provider_and_model_routes_gmi_to_generic_flow(self, monkeypatch): 317 recorded: dict[str, str] = {} 318 319 monkeypatch.setattr("hermes_cli.auth.resolve_provider", lambda *args, **kwargs: None) 320 321 def fake_prompt_provider_choice(choices, default=0): 322 return next(i for i, label in enumerate(choices) if label.startswith("GMI Cloud")) 323 324 def fake_model_flow_api_key_provider(config, provider_id, current_model=""): 325 recorded["provider_id"] = provider_id 326 327 monkeypatch.setattr("hermes_cli.main._prompt_provider_choice", fake_prompt_provider_choice) 328 monkeypatch.setattr("hermes_cli.main._model_flow_api_key_provider", fake_model_flow_api_key_provider) 329 330 from hermes_cli.main import select_provider_and_model 331 332 select_provider_and_model() 333 334 assert recorded["provider_id"] == "gmi" 335 336 def test_model_flow_api_key_provider_persists_gmi_selection(self, monkeypatch): 337 monkeypatch.setenv("GMI_API_KEY", "gmi-test-key") 338 339 with patch( 340 "hermes_cli.models.fetch_api_models", 341 return_value=["zai-org/GLM-5.1-FP8", "openai/gpt-5.4-mini"], 342 ), patch( 343 "hermes_cli.auth._prompt_model_selection", 344 return_value="openai/gpt-5.4-mini", 345 ), patch( 346 "hermes_cli.auth.deactivate_provider", 347 ), patch( 348 "builtins.input", 349 return_value="", 350 ): 351 from hermes_cli.main import _model_flow_api_key_provider 352 353 _model_flow_api_key_provider(load_config(), "gmi", "old-model") 354 355 import yaml 356 from hermes_constants import get_hermes_home 357 358 config = yaml.safe_load((get_hermes_home() / "config.yaml").read_text()) or {} 359 model_cfg = config.get("model") 360 assert isinstance(model_cfg, dict) 361 assert model_cfg["provider"] == "gmi" 362 assert model_cfg["default"] == "openai/gpt-5.4-mini" 363 assert model_cfg["base_url"] == "https://api.gmi-serving.com/v1"