fix(cli): validate user-defined providers consistently

This commit is contained in:
LeonSGP43 2026-04-24 10:15:28 +08:00 committed by Teknium
parent 3aa1a41e88
commit ccc8fccf77
4 changed files with 135 additions and 13 deletions

View file

@ -296,16 +296,33 @@ def run_doctor(args):
except Exception: except Exception:
pass pass
try: 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: 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 canonical_provider = provider
if provider and _resolve_provider is not None and provider != "auto": if provider and _resolve_provider_full is not None and provider != "auto":
try: provider_def = _resolve_provider_full(provider, user_providers, custom_providers)
canonical_provider = _resolve_provider(provider) canonical_provider = provider_def.id if provider_def is not None else None
except Exception:
canonical_provider = None
if provider and provider != "auto": if provider and provider != "auto":
if canonical_provider is None or (known_providers and canonical_provider not in known_providers): if canonical_provider is None or (known_providers and canonical_provider not in known_providers):

View file

@ -1429,6 +1429,7 @@ def select_provider_and_model(args=None):
load_config, load_config,
get_env_value, get_env_value,
) )
from hermes_cli.providers import resolve_provider_full
config = load_config() config = load_config()
current_model = config.get("model") current_model = config.get("model")
@ -1446,14 +1447,30 @@ def select_provider_and_model(args=None):
effective_provider = ( effective_provider = (
config_provider or os.getenv("HERMES_INFERENCE_PROVIDER") or "auto" config_provider or os.getenv("HERMES_INFERENCE_PROVIDER") or "auto"
) )
try: compatible_custom_providers = get_compatible_custom_providers(config)
active = resolve_provider(effective_provider) active = None
except AuthError as exc: if effective_provider != "auto":
warning = format_auth_error(exc) active_def = resolve_provider_full(
print(f"Warning: {warning} Falling back to auto provider detection.") 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: try:
active = resolve_provider("auto") 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 active = None # no provider yet; default to first in list
# Detect custom endpoint # Detect custom endpoint

View file

@ -3,6 +3,8 @@
import os import os
import sys import sys
import types import types
import io
import contextlib
from argparse import Namespace from argparse import Namespace
from types import SimpleNamespace 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 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): def test_run_doctor_termux_does_not_mark_browser_available_without_agent_browser(monkeypatch, tmp_path):
home = tmp_path / ".hermes" home = tmp_path / ".hermes"
home.mkdir(parents=True, exist_ok=True) home.mkdir(parents=True, exist_ok=True)

View file

@ -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 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): def test_codex_setup_uses_runtime_access_token_for_live_model_list(tmp_path, monkeypatch):
"""Codex model list fetching uses the runtime access token.""" """Codex model list fetching uses the runtime access token."""
monkeypatch.setenv("HERMES_HOME", str(tmp_path)) monkeypatch.setenv("HERMES_HOME", str(tmp_path))