mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-21 16:18:55 +00:00
feat(providers): add `enabled: false` flag to hide a provider
A ``providers.<name>`` block in ``config.yaml`` can now opt out of being listed anywhere by setting ``enabled: false`` — without removing the block, so re-enabling it stays a one-line edit. Missing or ``true`` keeps the previous behaviour (enabled), so this is fully backwards-compatible. The flag is honoured in four places: * ``hermes_cli/model_switch.py`` — model-override validation (the allow-list that the picker consults to accept a non-public model id) and the picker's own endpoint iteration. A disabled provider no longer appears as a row and its models can't be silently accepted via override. * ``hermes_cli/runtime_provider.py`` — the runtime resolver skips disabled blocks, so an explicit ``--provider X`` against a disabled entry fails fast instead of using stale base_url / api_key from the ignored block. * ``hermes_cli/doctor.py`` — the doctor's "configured providers" set excludes disabled entries, so health checks don't flag missing API keys for providers the user has turned off. Motivation: when a user has 20+ providers wired up in ``config.yaml`` (many of them only used occasionally) the picker becomes noisy and the runtime resolver may pick a suboptimal one on ambiguous --provider names. There's currently no way to hide a provider short of deleting its block — which loses the api_key + base_url + custom routing config the user spent time wiring. ``enabled: false`` lets them keep the config but get it out of the way. The helper ``is_provider_enabled()`` in ``hermes_cli/config.py`` centralises the gate (and accepts YAML-stringified booleans like ``"false"`` for hand-edited configs). 17 unit tests cover the defaults and edge cases. A follow-up PR can wire ``hermes provider enable/disable <name>`` and a dashboard toggle on top of this primitive — they reduce to mutating the flag.
This commit is contained in:
parent
b239ee2123
commit
7de06f700e
5 changed files with 81 additions and 1 deletions
|
|
@ -6963,6 +6963,31 @@ def _normalize_max_turns_config(config: Dict[str, Any]) -> Dict[str, Any]:
|
|||
return config
|
||||
|
||||
|
||||
def is_provider_enabled(provider_cfg: Optional[Dict[str, Any]]) -> bool:
|
||||
"""Return whether a ``providers.<name>`` config block is enabled.
|
||||
|
||||
A provider is enabled by default. Only an explicit ``enabled: false`` in
|
||||
the block hides it from the model picker, ``/models`` listings, the
|
||||
runtime resolver and the doctor / status output.
|
||||
|
||||
Backward-compat: configs without the ``enabled`` key keep working as
|
||||
before — the default is ``True``.
|
||||
|
||||
Pass any non-dict (None, list, string) and you get ``True`` too, so
|
||||
malformed entries don't disappear silently; they'll still be flagged
|
||||
by the existing validation paths.
|
||||
"""
|
||||
if not isinstance(provider_cfg, dict):
|
||||
return True
|
||||
flag = provider_cfg.get("enabled", True)
|
||||
if isinstance(flag, bool):
|
||||
return flag
|
||||
# YAML can produce strings for "true"/"false" depending on quoting.
|
||||
if isinstance(flag, str):
|
||||
return flag.strip().lower() not in {"false", "0", "no", "off"}
|
||||
return bool(flag)
|
||||
|
||||
|
||||
def cfg_get(cfg: Optional[Dict[str, Any]], *keys: str, default: Any = None) -> Any:
|
||||
"""Traverse nested dict keys safely, returning ``default`` on any miss.
|
||||
|
||||
|
|
|
|||
|
|
@ -869,7 +869,12 @@ def run_doctor(args):
|
|||
|
||||
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())
|
||||
from hermes_cli.config import is_provider_enabled
|
||||
known_providers.update(
|
||||
str(name).strip().lower()
|
||||
for name, prov_cfg in user_providers.items()
|
||||
if str(name).strip() and is_provider_enabled(prov_cfg)
|
||||
)
|
||||
for entry in custom_providers:
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -1450,8 +1450,11 @@ def switch_model(
|
|||
if not validation.get("accepted"):
|
||||
override = False
|
||||
if user_providers:
|
||||
from hermes_cli.config import is_provider_enabled
|
||||
# user_providers is a dict: {provider_slug: config_dict}
|
||||
for slug, cfg in user_providers.items():
|
||||
if not is_provider_enabled(cfg):
|
||||
continue
|
||||
if slug == target_provider:
|
||||
if new_model in _declared_model_ids(cfg.get("models", {})):
|
||||
override = True
|
||||
|
|
@ -2228,10 +2231,16 @@ def list_authenticated_providers(
|
|||
# the wire protocol differs.
|
||||
from collections import OrderedDict as _OD3
|
||||
|
||||
from hermes_cli.config import is_provider_enabled
|
||||
|
||||
ep_groups: "_OD3[tuple, dict]" = _OD3()
|
||||
for ep_name, ep_cfg in user_providers.items():
|
||||
if not isinstance(ep_cfg, dict):
|
||||
continue
|
||||
# Honour explicit ``providers.<name>.enabled: false`` from
|
||||
# config — these are hidden from the picker.
|
||||
if not is_provider_enabled(ep_cfg):
|
||||
continue
|
||||
if ep_name.lower() in seen_slugs:
|
||||
continue
|
||||
display_name = ep_cfg.get("name", "") or ep_name
|
||||
|
|
|
|||
|
|
@ -659,9 +659,16 @@ def _get_named_custom_provider(requested_provider: str) -> Optional[Dict[str, An
|
|||
# First check providers: dict (new-style user-defined providers)
|
||||
providers = config.get("providers")
|
||||
if isinstance(providers, dict):
|
||||
from hermes_cli.config import is_provider_enabled
|
||||
for ep_name, entry in providers.items():
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
# Skip providers the user explicitly disabled via
|
||||
# ``providers.<name>.enabled: false``. They remain in config
|
||||
# so re-enabling is a one-line edit, but the resolver pretends
|
||||
# they're not configured.
|
||||
if not is_provider_enabled(entry):
|
||||
continue
|
||||
# Match exact name or normalized name
|
||||
name_norm = _normalize_custom_provider_name(ep_name)
|
||||
# Resolve the API key from the env var name stored in key_env
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ from hermes_cli.config import (
|
|||
get_compatible_custom_providers,
|
||||
_explicit_config_paths,
|
||||
_normalize_max_turns_config,
|
||||
is_provider_enabled,
|
||||
load_config,
|
||||
load_env,
|
||||
migrate_config,
|
||||
|
|
@ -2240,3 +2241,36 @@ class TestCodexAppServerAutoConfig:
|
|||
assert raw["compression"]["codex_app_server_auto"] == "hermes"
|
||||
|
||||
|
||||
class TestIsProviderEnabled:
|
||||
"""``is_provider_enabled`` gates ``providers.<name>`` blocks for the
|
||||
model picker, ``/models`` listings and the runtime resolver. Default
|
||||
must be ``True`` so existing configs keep working untouched."""
|
||||
|
||||
def test_missing_flag_defaults_to_enabled(self):
|
||||
assert is_provider_enabled({"name": "Anthropic"}) is True
|
||||
|
||||
def test_empty_block_defaults_to_enabled(self):
|
||||
assert is_provider_enabled({}) is True
|
||||
|
||||
def test_explicit_true_is_enabled(self):
|
||||
assert is_provider_enabled({"enabled": True}) is True
|
||||
|
||||
def test_explicit_false_hides_it(self):
|
||||
assert is_provider_enabled({"enabled": False}) is False
|
||||
|
||||
@pytest.mark.parametrize("raw", ["false", "False", "FALSE", "0", "no", "off"])
|
||||
def test_yaml_string_falsy_values_hide_it(self, raw):
|
||||
# YAML can hand us a string for a value when the user quotes it.
|
||||
assert is_provider_enabled({"enabled": raw}) is False
|
||||
|
||||
@pytest.mark.parametrize("raw", ["true", "True", "yes", "on", "1", "anything-else"])
|
||||
def test_yaml_string_truthy_values_keep_it_enabled(self, raw):
|
||||
assert is_provider_enabled({"enabled": raw}) is True
|
||||
|
||||
def test_non_dict_input_defaults_to_enabled(self):
|
||||
# Malformed entries (None, list, string) don't disappear silently —
|
||||
# the gate stays open and the existing validation paths will flag
|
||||
# them.
|
||||
assert is_provider_enabled(None) is True
|
||||
assert is_provider_enabled([]) is True
|
||||
assert is_provider_enabled("oops") is True
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue