diff --git a/hermes_cli/doctor.py b/hermes_cli/doctor.py index 58b008d87..cba4ebcdd 100644 --- a/hermes_cli/doctor.py +++ b/hermes_cli/doctor.py @@ -296,16 +296,33 @@ def run_doctor(args): except Exception: pass try: - from hermes_cli.auth import resolve_provider as _resolve_provider + from hermes_cli.config import get_compatible_custom_providers as _compatible_custom_providers + from hermes_cli.providers import resolve_provider_full as _resolve_provider_full except Exception: - _resolve_provider = None + _compatible_custom_providers = None + _resolve_provider_full = None + + custom_providers = [] + if _compatible_custom_providers is not None: + try: + custom_providers = _compatible_custom_providers(cfg) + except Exception: + custom_providers = [] + + user_providers = cfg.get("providers") + if isinstance(user_providers, dict): + known_providers.update(str(name).strip().lower() for name in user_providers if str(name).strip()) + for entry in custom_providers: + if not isinstance(entry, dict): + continue + name = str(entry.get("name") or "").strip() + if name: + known_providers.add("custom:" + name.lower().replace(" ", "-")) canonical_provider = provider - if provider and _resolve_provider is not None and provider != "auto": - try: - canonical_provider = _resolve_provider(provider) - except Exception: - canonical_provider = None + if provider and _resolve_provider_full is not None and provider != "auto": + provider_def = _resolve_provider_full(provider, user_providers, custom_providers) + canonical_provider = provider_def.id if provider_def is not None else None if provider and provider != "auto": if canonical_provider is None or (known_providers and canonical_provider not in known_providers): diff --git a/hermes_cli/main.py b/hermes_cli/main.py index cadfd8b02..9a21cfa44 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -1429,6 +1429,7 @@ def select_provider_and_model(args=None): load_config, get_env_value, ) + from hermes_cli.providers import resolve_provider_full config = load_config() current_model = config.get("model") @@ -1446,14 +1447,30 @@ def select_provider_and_model(args=None): effective_provider = ( config_provider or os.getenv("HERMES_INFERENCE_PROVIDER") or "auto" ) - try: - active = resolve_provider(effective_provider) - except AuthError as exc: - warning = format_auth_error(exc) - print(f"Warning: {warning} Falling back to auto provider detection.") + compatible_custom_providers = get_compatible_custom_providers(config) + active = None + if effective_provider != "auto": + active_def = resolve_provider_full( + effective_provider, + config.get("providers"), + compatible_custom_providers, + ) + if active_def is not None: + active = active_def.id + else: + warning = ( + f"Unknown provider '{effective_provider}'. Check 'hermes model' for " + "available providers, or run 'hermes doctor' to diagnose config " + "issues." + ) + print(f"Warning: {warning} Falling back to auto provider detection.") + if active is None: try: active = resolve_provider("auto") - except AuthError: + except AuthError as exc: + if effective_provider == "auto": + warning = format_auth_error(exc) + print(f"Warning: {warning} Falling back to auto provider detection.") active = None # no provider yet; default to first in list # Detect custom endpoint diff --git a/tests/hermes_cli/test_doctor.py b/tests/hermes_cli/test_doctor.py index 948cafaf7..37cad8516 100644 --- a/tests/hermes_cli/test_doctor.py +++ b/tests/hermes_cli/test_doctor.py @@ -3,6 +3,8 @@ import os import sys import types +import io +import contextlib from argparse import Namespace from types import SimpleNamespace @@ -255,6 +257,57 @@ def test_run_doctor_termux_treats_docker_and_browser_warnings_as_expected(monkey assert "docker not found (optional)" not in out +def test_run_doctor_accepts_named_provider_from_providers_section(monkeypatch, tmp_path): + home = tmp_path / ".hermes" + home.mkdir(parents=True, exist_ok=True) + + import yaml + + (home / "config.yaml").write_text( + yaml.dump( + { + "model": { + "provider": "volcengine-plan", + "default": "doubao-seed-2.0-code", + }, + "providers": { + "volcengine-plan": { + "name": "volcengine-plan", + "base_url": "https://ark.cn-beijing.volces.com/api/coding/v3", + "default_model": "doubao-seed-2.0-code", + "models": {"doubao-seed-2.0-code": {}}, + } + }, + } + ) + ) + + monkeypatch.setattr(doctor_mod, "HERMES_HOME", home) + monkeypatch.setattr(doctor_mod, "PROJECT_ROOT", tmp_path / "project") + monkeypatch.setattr(doctor_mod, "_DHH", str(home)) + (tmp_path / "project").mkdir(exist_ok=True) + + fake_model_tools = types.SimpleNamespace( + check_tool_availability=lambda *a, **kw: ([], []), + TOOLSET_REQUIREMENTS={}, + ) + monkeypatch.setitem(sys.modules, "model_tools", fake_model_tools) + + try: + from hermes_cli import auth as _auth_mod + monkeypatch.setattr(_auth_mod, "get_nous_auth_status", lambda: {}) + monkeypatch.setattr(_auth_mod, "get_codex_auth_status", lambda: {}) + except Exception: + pass + + buf = io.StringIO() + with contextlib.redirect_stdout(buf): + doctor_mod.run_doctor(Namespace(fix=False)) + + out = buf.getvalue() + assert "model.provider 'volcengine-plan' is not a recognised provider" not in out + + def test_run_doctor_termux_does_not_mark_browser_available_without_agent_browser(monkeypatch, tmp_path): home = tmp_path / ".hermes" home.mkdir(parents=True, exist_ok=True) diff --git a/tests/hermes_cli/test_setup.py b/tests/hermes_cli/test_setup.py index 150fddab0..03b406875 100644 --- a/tests/hermes_cli/test_setup.py +++ b/tests/hermes_cli/test_setup.py @@ -339,6 +339,41 @@ def test_select_provider_and_model_warns_if_named_custom_provider_disappears( assert "selected saved custom provider is no longer available" in out +def test_select_provider_and_model_accepts_named_provider_from_providers_section( + tmp_path, monkeypatch, capsys +): + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + _clear_provider_env(monkeypatch) + + cfg = load_config() + cfg["model"] = { + "provider": "volcengine-plan", + "default": "doubao-seed-2.0-code", + } + cfg["providers"] = { + "volcengine-plan": { + "name": "volcengine-plan", + "base_url": "https://ark.cn-beijing.volces.com/api/coding/v3", + "default_model": "doubao-seed-2.0-code", + "models": {"doubao-seed-2.0-code": {}}, + } + } + save_config(cfg) + + monkeypatch.setattr( + "hermes_cli.main._prompt_provider_choice", + lambda choices, default=0: len(choices) - 1, + ) + + from hermes_cli.main import select_provider_and_model + + select_provider_and_model() + + out = capsys.readouterr().out + assert "Warning: Unknown provider 'volcengine-plan'" not in out + assert "Active provider: volcengine-plan" in out + + def test_codex_setup_uses_runtime_access_token_for_live_model_list(tmp_path, monkeypatch): """Codex model list fetching uses the runtime access token.""" monkeypatch.setenv("HERMES_HOME", str(tmp_path))