mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
feat(providers): extend `enabled: false` gate to built-in resolution
The first commit's gate sat inside ``_get_named_custom_provider`` — which only handles user-defined custom blocks. Built-in provider names (``openai`` / ``anthropic`` / ``openrouter`` / ``gemini`` / ...) have their own resolution paths in ``resolve_runtime_provider`` (pool / explicit / generic / ``resolve_provider``) and bypass that gate. So a user who flipped ``providers.openrouter.enabled: false`` would still see OpenRouter resolved when something explicitly requested it (e.g. a fallback chain entry). That defeats the point of the flag. This commit moves the gate one level up: right after ``requested_provider`` is computed, before any custom / built-in / Azure short-circuit. It now raises a typed ``ValueError`` referencing the YAML path, so callers can recognise it and advance to the next fallback instead of silently using a disabled provider. 3 new tests cover: * disabled custom provider raises * disabled built-in provider raises * enabled provider doesn't hit the gate All 20 tests in the providers suite pass.
This commit is contained in:
parent
7de06f700e
commit
305ecac8b2
2 changed files with 102 additions and 0 deletions
|
|
@ -1538,6 +1538,27 @@ def resolve_runtime_provider(
|
|||
"""
|
||||
requested_provider = resolve_requested_provider(requested)
|
||||
|
||||
# Honour ``providers.<name>.enabled: false`` for BOTH user-defined
|
||||
# custom providers and the built-in ones (openai / anthropic /
|
||||
# openrouter / gemini / ...). The earlier ``_get_named_custom_provider``
|
||||
# gate only covers custom blocks — built-in resolution paths
|
||||
# (``resolve_provider`` + pool / explicit / generic runtime) walk
|
||||
# their own short-circuits and would otherwise return stale config
|
||||
# for a provider the user explicitly turned off.
|
||||
#
|
||||
# Fail fast with a typed error so the fallback chain can advance to
|
||||
# the next provider instead of using a disabled one.
|
||||
from hermes_cli.config import is_provider_enabled, load_config
|
||||
_full_cfg = load_config()
|
||||
_provs_cfg = _full_cfg.get("providers") if isinstance(_full_cfg, dict) else None
|
||||
if isinstance(_provs_cfg, dict):
|
||||
_block = _provs_cfg.get(requested_provider)
|
||||
if isinstance(_block, dict) and not is_provider_enabled(_block):
|
||||
raise ValueError(
|
||||
f"provider {requested_provider!r} is disabled in config "
|
||||
f"(providers.{requested_provider}.enabled: false)"
|
||||
)
|
||||
|
||||
if requested_provider == "moa":
|
||||
return {
|
||||
"provider": "moa",
|
||||
|
|
|
|||
|
|
@ -2274,3 +2274,84 @@ class TestIsProviderEnabled:
|
|||
assert is_provider_enabled(None) is True
|
||||
assert is_provider_enabled([]) is True
|
||||
assert is_provider_enabled("oops") is True
|
||||
|
||||
|
||||
class TestProviderEnabledRuntimeGate:
|
||||
"""Verify ``resolve_runtime_provider`` honours ``enabled: false`` for
|
||||
both custom-defined and built-in provider names. Smoke test only —
|
||||
full runtime resolution has its own fixture-heavy tests; here we
|
||||
only assert the early-exit raises a typed error."""
|
||||
|
||||
def test_disabled_custom_provider_raises_valueerror(self, tmp_path, monkeypatch):
|
||||
cfg = {
|
||||
"model": {"default": "claude-sonnet-4-6", "provider": "claude-agent-sdk"},
|
||||
"providers": {
|
||||
"my-fork": {
|
||||
"name": "my-fork",
|
||||
"base_url": "http://127.0.0.1:9999",
|
||||
"api_key": "not-needed",
|
||||
"enabled": False,
|
||||
},
|
||||
},
|
||||
}
|
||||
config_path = tmp_path / "config.yaml"
|
||||
config_path.write_text(yaml.safe_dump(cfg))
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
# Bust the in-process config cache so the override picks up.
|
||||
from hermes_cli import config as cfg_mod
|
||||
cfg_mod._cached_config = None # type: ignore[attr-defined]
|
||||
|
||||
from hermes_cli.runtime_provider import resolve_runtime_provider
|
||||
with pytest.raises(ValueError, match="disabled"):
|
||||
resolve_runtime_provider(requested="my-fork")
|
||||
|
||||
def test_disabled_builtin_provider_raises_valueerror(self, tmp_path, monkeypatch):
|
||||
# `openrouter` is a built-in name with its own resolution path —
|
||||
# the gate must fire BEFORE that path runs.
|
||||
cfg = {
|
||||
"model": {"default": "claude-sonnet-4-6", "provider": "claude-agent-sdk"},
|
||||
"providers": {
|
||||
"openrouter": {
|
||||
"name": "OpenRouter",
|
||||
"base_url": "https://openrouter.ai/api/v1",
|
||||
"enabled": False,
|
||||
},
|
||||
},
|
||||
}
|
||||
config_path = tmp_path / "config.yaml"
|
||||
config_path.write_text(yaml.safe_dump(cfg))
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
from hermes_cli import config as cfg_mod
|
||||
cfg_mod._cached_config = None # type: ignore[attr-defined]
|
||||
|
||||
from hermes_cli.runtime_provider import resolve_runtime_provider
|
||||
with pytest.raises(ValueError, match="disabled"):
|
||||
resolve_runtime_provider(requested="openrouter")
|
||||
|
||||
def test_enabled_provider_does_not_raise(self, tmp_path, monkeypatch):
|
||||
cfg = {
|
||||
"model": {"default": "claude-sonnet-4-6", "provider": "claude-agent-sdk"},
|
||||
"providers": {
|
||||
"claude-agent-sdk": {
|
||||
"name": "Claude Agent SDK",
|
||||
"base_url": "http://127.0.0.1:3456",
|
||||
"api_key": "not-needed",
|
||||
"enabled": True,
|
||||
},
|
||||
},
|
||||
}
|
||||
config_path = tmp_path / "config.yaml"
|
||||
config_path.write_text(yaml.safe_dump(cfg))
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
from hermes_cli import config as cfg_mod
|
||||
cfg_mod._cached_config = None # type: ignore[attr-defined]
|
||||
|
||||
# Don't assert success — built-in resolution needs more state.
|
||||
# We only assert this path doesn't hit the disabled-gate.
|
||||
from hermes_cli.runtime_provider import resolve_runtime_provider
|
||||
try:
|
||||
resolve_runtime_provider(requested="claude-agent-sdk")
|
||||
except ValueError as e:
|
||||
assert "disabled" not in str(e).lower()
|
||||
except Exception:
|
||||
pass # any non-ValueError is fine; we only gate the disabled path
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue