simplify(compat): interrupt — drop _ThreadAwareEventProxy/_interrupt_event legacy alias, repoint 2 test files

No runtime consumer read the proxy (terminal_tool/environments call is_interrupted()/set_interrupt()
directly); its only users were tests patching tools.interrupt._interrupt_event, which had no effect on
the code under test. tools/terminal_tool.py's own re-export of the name is owned by another worker.
This commit is contained in:
Teknium
2026-09-03 14:00:59 -07:00
parent c93ace77c2
commit 7a33369e81
109 changed files with 511 additions and 403 deletions
+1 -2
View File
@@ -25,8 +25,7 @@ def _named_custom_provider_catalogs() -> list[tuple[str, str, list[tuple[str, st
discovery (some endpoints have no ``/models`` route). Slugs use the ``custom:<name>`` shape
``parse_model_input``/``resolve_runtime_provider`` resolve, so choice ids round-trip."""
try:
from hermes_cli.config import load_config
from hermes_cli.config_providers import get_compatible_custom_providers, is_provider_enabled
from hermes_cli.config import (get_compatible_custom_providers, is_provider_enabled, load_config)
from hermes_cli.model_switch import _declared_model_ids, _entry_models_discovered, _models_config_is_allowlist
from hermes_cli.model_switch_providers import _NativePickerModelList, _fetch_picker_live_models
from hermes_cli.model_switch_providers import _discover_flag
+7 -7
View File
@@ -886,10 +886,10 @@ def _apply_openai_header_policy(agent, client_kwargs: Dict[str, Any]) -> None:
# model.default_headers override provider/SDK defaults (WAFs rejecting SDK headers).
agent._apply_user_default_headers()
try:
from hermes_cli.config import load_config
from hermes_cli.config_providers import (
apply_custom_provider_extra_headers_to_client_kwargs, apply_custom_provider_tls_to_client_kwargs,
get_compatible_custom_providers
from hermes_cli.config import (
apply_custom_provider_extra_headers_to_client_kwargs,
apply_custom_provider_tls_to_client_kwargs, get_compatible_custom_providers,
load_config,
)
_cp_entries = get_compatible_custom_providers(load_config())
_cp_base_url = str(client_kwargs.get("base_url") or agent.base_url or "")
@@ -1519,7 +1519,7 @@ def _custom_provider_configured_base_url(
_user_providers = _agent_cfg.get("providers")
_disabled_ids: set[str] = set()
if isinstance(_user_providers, dict):
from hermes_cli.config_providers import is_provider_enabled
from hermes_cli.config import is_provider_enabled
for _key, _entry in _user_providers.items():
if not isinstance(_entry, dict):
continue
@@ -1696,7 +1696,7 @@ def _resolve_context_length(agent, _agent_cfg, base_url):
# Resolve custom_providers before route-scoping: a named provider may keep its URL here.
try:
from hermes_cli.config_providers import get_compatible_custom_providers
from hermes_cli.config import get_compatible_custom_providers
_custom_providers = get_compatible_custom_providers(_agent_cfg)
except Exception:
_custom_providers = _agent_cfg.get("custom_providers")
@@ -1716,7 +1716,7 @@ def _resolve_context_length(agent, _agent_cfg, base_url):
if _config_context_length is None and _custom_providers:
with suppress(Exception):
from hermes_cli.config_providers import get_custom_provider_context_length
from hermes_cli.config import get_custom_provider_context_length
_cp_ctx_resolved = get_custom_provider_context_length(
model=agent.model, base_url=agent.base_url, custom_providers=_custom_providers
)
+9 -10
View File
@@ -1521,7 +1521,7 @@ def anthropic_prompt_cache_policy(
or _route_may_be_custom(agent, eff_provider, provider_lower, eff_base_url)
):
try:
from hermes_cli.config_providers import get_custom_provider_model_capability
from hermes_cli.config import get_custom_provider_model_capability
custom_prompt_caching = get_custom_provider_model_capability(
model=eff_model, base_url=eff_base_url, capability="prompt_caching",
custom_providers=getattr(agent, "_custom_providers", None),
@@ -1768,8 +1768,7 @@ def _apply_switched_provider_request_overrides(agent, new_provider):
custom_providers = getattr(agent, "_custom_providers", None)
if custom_providers is None:
try:
from hermes_cli.config import load_config
from hermes_cli.config_providers import get_compatible_custom_providers
from hermes_cli.config import load_config, get_compatible_custom_providers
custom_providers = get_compatible_custom_providers(load_config())
except Exception:
custom_providers = []
@@ -1892,9 +1891,9 @@ def _build_switched_client(agent, new_provider, api_key, base_url, api_mode, new
effective_base = base_url or agent.base_url
agent._client_kwargs = {"api_key": api_key or agent.api_key, "base_url": effective_base}
try:
from hermes_cli.config import load_config_readonly
from hermes_cli.config_providers import (
apply_custom_provider_tls_to_client_kwargs, get_compatible_custom_providers
from hermes_cli.config import (
apply_custom_provider_tls_to_client_kwargs, get_compatible_custom_providers,
load_config_readonly,
)
# Read live config, not agent._custom_providers, so mid-session ssl_ca_cert / ssl_verify
# edits are honored.
@@ -1963,8 +1962,9 @@ def _resolve_switch_context_length(agent, snapshot):
"""Resolve the destination context length (LM Studio preload first); returns ``(custom_providers, effective_len)``."""
custom_providers = None
try:
from hermes_cli.config import load_config
from hermes_cli.config_providers import get_compatible_custom_providers, get_custom_provider_context_length
from hermes_cli.config import (
get_compatible_custom_providers, get_custom_provider_context_length, load_config
)
custom_providers = get_compatible_custom_providers(load_config())
intent = get_custom_provider_context_length(
model=agent.model, base_url=agent.base_url, custom_providers=custom_providers
@@ -1996,8 +1996,7 @@ def _update_switch_compressor(agent, custom_providers, effective_context_length,
from agent.model_metadata import get_model_context_length
if custom_providers is None:
try:
from hermes_cli.config import load_config
from hermes_cli.config_providers import get_compatible_custom_providers
from hermes_cli.config import get_compatible_custom_providers, load_config
custom_providers = get_compatible_custom_providers(load_config())
except Exception:
custom_providers = None
+3 -4
View File
@@ -130,8 +130,7 @@ def _resolve_aux_verify(base_url: Optional[str]) -> Any:
``ssl_verify``, ``HERMES_CA_BUNDLE`` / ``SSL_CERT_FILE``); any failure → httpx default (``True``)."""
try:
from agent.ssl_verify import resolve_httpx_verify
from hermes_cli.config import load_config_readonly
from hermes_cli.config_providers import get_custom_provider_tls_settings
from hermes_cli.config import get_custom_provider_tls_settings, load_config_readonly
tls = get_custom_provider_tls_settings(str(base_url or ""), config=load_config_readonly())
return resolve_httpx_verify(
ca_bundle=tls.get("ssl_ca_cert"), ssl_verify=tls.get("ssl_verify"), base_url=str(base_url or ""))
@@ -4080,7 +4079,7 @@ def _try_main_provider_route(
# Named custom provider (custom_providers / providers dict entry).
_has_named_entry = False
with contextlib.suppress(ImportError):
from hermes_cli.runtime_provider_custom import _get_named_custom_provider
from hermes_cli.runtime_provider import _get_named_custom_provider
_has_named_entry = _get_named_custom_provider(main_provider) is not None
if _has_named_entry:
# KEEP the full ``custom:<name>`` so the named arm honours the entry's api_mode
@@ -4546,7 +4545,7 @@ def _named_custom_openai_wire_client(custom_base: str, custom_key: Any):
def _resolve_named_custom_branch(req: _ResolveRequest) -> Optional[_ResolveResult]:
"""Named custom provider (config.yaml providers dict / custom_providers list); None if no entry matches."""
from hermes_cli.runtime_provider_custom import _get_named_custom_provider
from hermes_cli.runtime_provider import _get_named_custom_provider
provider = req.provider
# If the raw name is an alias (``kimi`` → ``kimi-coding``) and a custom_providers entry exists
# under it, the custom entry wins over alias rewriting. Only for aliases, so entries matching a
+4 -5
View File
@@ -578,7 +578,7 @@ class ClientLifecycleMixin:
elif self.provider == "custom":
# Named custom provider: identity in config, credential in key_env; no key_env → nothing to watch.
try:
from hermes_cli.runtime_provider_custom import _get_named_custom_provider
from hermes_cli.runtime_provider import _get_named_custom_provider
except ImportError:
return None
custom_provider = _get_named_custom_provider(getattr(self, "requested_provider", "") or "")
@@ -791,7 +791,7 @@ class ClientLifecycleMixin:
# Per-provider extra_headers last so they survive swaps/rebuilds. SECURITY: may carry credentials; never log.
if self.api_mode not in ("anthropic_messages", "bedrock_converse"):
try:
from hermes_cli.config_providers import apply_custom_provider_extra_headers_to_client_kwargs
from hermes_cli.config import apply_custom_provider_extra_headers_to_client_kwargs
apply_custom_provider_extra_headers_to_client_kwargs(self._client_kwargs, base_url)
except Exception:
logger.debug("custom-provider extra_headers skipped", exc_info=True)
@@ -836,9 +836,8 @@ class ClientLifecycleMixin:
self._client_kwargs.pop("ssl_verify", None)
self._client_kwargs.pop("ssl_ca_cert", None)
try:
from hermes_cli.config import load_config_readonly
from hermes_cli.config_providers import (
apply_custom_provider_tls_to_client_kwargs, get_compatible_custom_providers
from hermes_cli.config import (
apply_custom_provider_tls_to_client_kwargs, get_compatible_custom_providers, load_config_readonly,
)
apply_custom_provider_tls_to_client_kwargs(
self._client_kwargs, str(self.base_url or ""), get_compatible_custom_providers(load_config_readonly()),
+1 -1
View File
@@ -450,7 +450,7 @@ def _iter_custom_providers(config: Optional[dict] = None):
if config is None:
return
try:
from hermes_cli.config_providers import get_compatible_custom_providers
from hermes_cli.config import get_compatible_custom_providers
custom_providers = get_compatible_custom_providers(config)
except Exception:
+3 -4
View File
@@ -52,7 +52,7 @@ def _resolve_requests_verify(base_url: str = "") -> bool | str:
spurious CERTIFICATE_VERIFY_FAILED while the httpx chat path succeeds) -> CA env vars -> certifi."""
if base_url:
try:
from hermes_cli.config_providers import get_custom_provider_tls_settings
from hermes_cli.config import get_custom_provider_tls_settings
tls = get_custom_provider_tls_settings(base_url)
if tls.get("ssl_verify") is False:
return False
@@ -1739,8 +1739,7 @@ def _resolve_moa_context_length(model: str, custom_providers: list | None) -> Op
"""Step 0a: MoA virtual provider — ``model`` is a preset name, so every probe would miss. Resolve
the aggregator's real provider+model (references are advisory). None on any failure."""
try:
from hermes_cli.config import load_config
from hermes_cli.config_providers import get_compatible_custom_providers
from hermes_cli.config import get_compatible_custom_providers, load_config
from hermes_cli.moa_config import resolve_moa_preset
from hermes_cli.runtime_provider import resolve_runtime_provider
config = load_config()
@@ -1777,7 +1776,7 @@ def _config_override_context_length(model: str, base_url: str, provider: str, cu
# set. See #15779.
if custom_providers and base_url and model:
with contextlib.suppress(Exception): # fall through to probing
from hermes_cli.config_providers import get_custom_provider_context_length
from hermes_cli.config import get_custom_provider_context_length
cp_ctx = get_custom_provider_context_length(model=model, base_url=base_url, custom_providers=custom_providers)
if cp_ctx:
return cp_ctx
+5 -3
View File
@@ -138,8 +138,10 @@ def _restore_plugin_prompt_sections(prompt: str) -> tuple:
"""Recover frozen section bytes from the persisted full prompt. Only the
exact canonical container emitted by core is accepted — user/project text
may resemble a frame."""
from hermes_cli.plugins import MAX_SYSTEM_PROMPT_SECTION_CHARS, RenderedPluginSystemPromptSection
from hermes_cli.plugins_dispatch import PLUGIN_SECTIONS_END, PLUGIN_SECTIONS_START, format_system_prompt_sections
from hermes_cli.plugins import (
MAX_SYSTEM_PROMPT_SECTION_CHARS, PLUGIN_SECTIONS_END, PLUGIN_SECTIONS_START,
RenderedPluginSystemPromptSection, format_system_prompt_sections,
)
start = prompt.rfind(PLUGIN_SECTIONS_START)
end = prompt.find(PLUGIN_SECTIONS_END, start + len(PLUGIN_SECTIONS_START)) if start >= 0 else -1
if end < 0:
@@ -165,7 +167,7 @@ def restore_plugin_prompt_sections(agent: Any, prompt: str) -> None:
def _plugin_section_blocks(sections: tuple, position: str) -> List[str]:
from hermes_cli.plugins_dispatch import format_system_prompt_sections
from hermes_cli.plugins import format_system_prompt_sections
block = format_system_prompt_sections([s for s in sections if s.position == position])
return [block] if block else []
+1 -1
View File
@@ -2693,7 +2693,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin, CLITuiMix
# Explicit `-m` still wins. See #86978.
if not model and provider:
try:
from hermes_cli.runtime_provider_custom import _get_named_custom_provider
from hermes_cli.runtime_provider import _get_named_custom_provider
_named_custom = _get_named_custom_provider(provider)
except Exception as exc:
+1 -1
View File
@@ -122,7 +122,7 @@ def _normalize_slack_parent_command(text: str, message_type: MessageType) -> tup
if not parent_parts or parent_parts[0] != "/hermes":
return text, message_type
from hermes_cli.commands_platforms import slack_subcommand_map
from hermes_cli.commands import slack_subcommand_map
payload = parent_parts[1].strip() if len(parent_parts) > 1 else ""
subcommand_map = slack_subcommand_map()
+3 -3
View File
@@ -829,7 +829,7 @@ def _telegramize_command_mentions(text: str, platform: Any) -> str:
if platform_value != "telegram":
return text
from hermes_cli.commands_platforms import _sanitize_telegram_name
from hermes_cli.commands import _sanitize_telegram_name
def _replace(match: re.Match[str]) -> str:
sanitized = _sanitize_telegram_name(match.group(1))
@@ -2227,7 +2227,7 @@ def _resolve_gateway_model_context(model: Optional[str] = None) -> _GatewayModel
configured_provider = provider = model_cfg.get("provider") or None
configured_base_url = base_url = model_cfg.get("base_url") or None
try:
from hermes_cli.config_providers import get_compatible_custom_providers
from hermes_cli.config import get_compatible_custom_providers
custom_providers = get_compatible_custom_providers(data)
except Exception:
custom_providers = data.get("custom_providers")
@@ -2246,7 +2246,7 @@ def _resolve_gateway_model_context(model: Optional[str] = None) -> _GatewayModel
configured_model, resolved_model, configured_base_url, base_url, configured_provider, provider)
def _custom_ctx() -> Optional[int]:
from hermes_cli.config_providers import get_custom_provider_context_length
from hermes_cli.config import get_custom_provider_context_length
return get_custom_provider_context_length(
model=resolved_model, base_url=base_url, custom_providers=custom_providers)
+2 -3
View File
@@ -25,8 +25,7 @@ from gateway.restart import (
)
from gateway.session import SessionSource
from gateway.session_state import SERVICE_TIER_UNSET as _SERVICE_TIER_UNSET
from hermes_cli.config import cfg_get
from hermes_cli.personality import resolve_ephemeral_system_prompt
from hermes_cli.config import cfg_get, resolve_ephemeral_system_prompt_from_config
from hermes_cli.fallback_config import get_fallback_chain
from utils import is_truthy_value
@@ -94,7 +93,7 @@ class GatewayConfigLoadersMixin:
prompt = os.getenv("HERMES_EPHEMERAL_SYSTEM_PROMPT", "")
if prompt:
return prompt
return resolve_ephemeral_system_prompt(_load_gateway_runtime_config())
return resolve_ephemeral_system_prompt_from_config(_load_gateway_runtime_config())
def _channel_override(self, platform: Platform, chat_id: str, thread_id, parent_id):
"""``channel_overrides`` entry for this channel/thread, or None (also when no config is bound)."""
+2 -2
View File
@@ -1520,7 +1520,7 @@ class GatewayInboundMixin:
if _msg_raw_ctx is not None:
_msg_config_ctx = int(_msg_raw_ctx)
try:
from hermes_cli.config_providers import get_compatible_custom_providers
from hermes_cli.config import get_compatible_custom_providers
_msg_custom_providers = get_compatible_custom_providers(_msg_cfg)
except Exception:
@@ -1550,7 +1550,7 @@ class GatewayInboundMixin:
_msg_config_ctx = None
if _msg_custom_providers and _msg_base_url:
with suppress(Exception):
from hermes_cli.config_providers import get_custom_provider_context_length
from hermes_cli.config import get_custom_provider_context_length
_msg_config_ctx = get_custom_provider_context_length(
model=_msg_model, base_url=_msg_base_url, custom_providers=_msg_custom_providers,
+2 -2
View File
@@ -586,9 +586,9 @@ class GatewayTurnMixin:
if hs.config_context_length is None and hs.base_url:
with suppress(TypeError, ValueError):
try:
from hermes_cli.config_providers import (
from hermes_cli.config import (
get_compatible_custom_providers as _gw_gcp,
get_custom_provider_context_length as _gw_gccl
get_custom_provider_context_length as _gw_gccl,
)
_hyg_custom_providers = _gw_gcp(hs.data)
except Exception:
+1 -1
View File
@@ -129,7 +129,7 @@ class _ModelSwitchContext:
self.current_base_url = model_cfg.get("base_url", "")
self.user_provs = cfg.get("providers")
try:
from hermes_cli.config_providers import get_compatible_custom_providers
from hermes_cli.config import get_compatible_custom_providers
self.custom_provs = get_compatible_custom_providers(cfg)
except Exception:
self.custom_provs = cfg.get("custom_providers")
+1 -2
View File
@@ -30,8 +30,7 @@ _OAUTH_CAPABLE_PROVIDERS = {"anthropic", "nous", "openai-codex", "xai-oauth", "q
def _get_custom_provider_entries() -> list[dict]:
"""Return configured provider entries with legacy and canonical pool IDs."""
try:
from hermes_cli.config import load_config
from hermes_cli.config_providers import get_compatible_custom_providers
from hermes_cli.config import get_compatible_custom_providers, load_config
config = load_config()
except Exception:
return []
+1 -1
View File
@@ -38,7 +38,7 @@ def _heal_bare_custom_provider(provider, *, base_url, model):
# Heal bare "custom" persisted by older builds / gateway turns: it's the resolved billing class, not
# a routable identity. (Stricter than the TUI gateway's recovery, which keeps bare "custom" when a
# base_url exists — the CLI's resolve path would hard-fail on it, #14676.)
from hermes_cli.runtime_provider_custom import canonical_custom_identity
from hermes_cli.runtime_provider import canonical_custom_identity
return canonical_custom_identity(base_url=base_url or None, model=model or None) or None
except Exception:
return None
+1 -1
View File
@@ -15,7 +15,7 @@ import threading
import time
from agent.interrupt_compat import request_hard_interrupt
from hermes_cli.commands_completion import SlashCommandAutoSuggest, SlashCommandCompleter
from hermes_cli.commands import SlashCommandAutoSuggest, SlashCommandCompleter
from pathlib import Path
from prompt_toolkit.filters import Condition
from prompt_toolkit.history import FileHistory
+23
View File
@@ -467,3 +467,26 @@ def _iter_plugin_command_entries() -> list[tuple[str, str, str]]:
return [(name, str(meta.get("description") or f"Run /{name}"),
str(meta.get("args_hint") or "").strip())
for name, meta in commands.items() if isinstance(name, str) and isinstance(meta, dict)]
# Lazy re-exports (PEP 562): platform derivations and the prompt_toolkit completer live in
# sibling modules; their names stay importable here (``from hermes_cli.commands import X``,
# ``patch("hermes_cli.commands.X")``) while this module stays import-light for the gateway.
_LAZY_EXPORTS: dict[str, str] = {name: "hermes_cli.commands_platforms" for name in (
"_CMD_NAME_LIMIT", "_DEFAULT_TELEGRAM_MENU_MAX_COMMANDS", "_SLACK_RESERVED_COMMANDS",
"_SLACK_VIA_HERMES_ONLY", "_TELEGRAM_MENU_PRIORITY", "_clamp_command_names",
"_collect_gateway_skill_entries", "_iter_gateway_skills", "_prioritize_telegram_menu_candidates",
"_requires_argument", "_sanitize_slack_name", "_sanitize_telegram_name",
"_telegram_command_menu_config", "_truncate_desc", "discord_skill_commands_by_category",
"slack_app_manifest", "slack_native_slashes", "slack_subcommand_map", "telegram_bot_commands",
"telegram_menu_commands", "telegram_menu_max_commands")}
_LAZY_EXPORTS.update({name: "hermes_cli.commands_completion" for name in (
"SlashCommandCompleter", "SlashCommandAutoSuggest", "_file_size_label", "_short_desc")})
def __getattr__(name: str):
module = _LAZY_EXPORTS.get(name)
if module is None:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
import importlib
return getattr(importlib.import_module(module), name)
+1 -1
View File
@@ -1,5 +1,5 @@
"""prompt_toolkit completer + inline auto-suggest for slash commands. Kept out of
:mod:`hermes_cli.commands` so the registry module stays
:mod:`hermes_cli.commands` (which re-exports both classes) so the registry module stays
prompt_toolkit-free for the gateway."""
from __future__ import annotations
+6 -2
View File
@@ -148,7 +148,9 @@ def _prioritize_telegram_menu_candidates(
"""Order ``(final_name, description, source, raw_name)`` candidates; the default priority
applies to core only, "replace" mode ignores it. ``raw_name`` is the pre-clamp name so a
configured long command stays addressable."""
menu_cfg = _telegram_command_menu_config()
# Lazy origin import: tests patch ``hermes_cli.commands._telegram_command_menu_config``.
from hermes_cli.commands import _telegram_command_menu_config as menu_config
menu_cfg = menu_config()
configured_rank = _sanitized_rank(menu_cfg["priority"])
default_rank = _sanitized_rank(_TELEGRAM_MENU_PRIORITY)
tiers = _TELEGRAM_PRIORITY_TIERS[menu_cfg["priority_mode"]]
@@ -270,7 +272,9 @@ def telegram_menu_commands(max_commands: int = 100) -> tuple[list[tuple[str, str
telegram-disabled skills excluded). Tiers keep relative order unless named in
``platforms.telegram.extra.command_menu.priority`` — applied *before* the cap, so a
prioritized dynamic command can displace an unprioritized core command."""
core_commands = list(telegram_bot_commands(include_plugins=False))
# Lazy origin import: tests patch ``hermes_cli.commands.telegram_bot_commands``.
from hermes_cli.commands import telegram_bot_commands as bot_commands
core_commands = list(bot_commands(include_plugins=False))
entries, hidden_count = _collect_gateway_skill_entries(
platform="telegram", max_slots=None, reserved_names={n for n, _ in core_commands},
desc_limit=40, sanitize_name=_sanitize_telegram_name)
+17 -1
View File
@@ -29,7 +29,7 @@ from hermes_cli import managed_scope
from hermes_cli.default_soul import DEFAULT_SOUL_MD, is_legacy_template_soul
from hermes_cli.secret_prompt import masked_secret_prompt
# Re-export from hermes_constants — canonical definition lives there.
from hermes_constants import get_hermes_home
from hermes_constants import get_hermes_home, get_process_hermes_home # noqa: F401
from utils import atomic_replace, atomic_yaml_write, fast_safe_load
logger = logging.getLogger(__name__)
@@ -686,7 +686,23 @@ def ensure_hermes_home():
# ---- Config loading/saving ----
from hermes_cli.config_defaults import DEFAULT_CONFIG, OPTIONAL_ENV_VARS # noqa: E402,F401
from hermes_cli.config_providers import ( # noqa: E402,F401 (re-exported; callers/tests use hermes_cli.config.<name>)
_API_MODE_ALIASES, _CAMEL_ALIASES, _KNOWN_PROVIDER_KEYS, _PROVIDER_NORMALIZE_WARNED,
_canonical_api_mode, _coerce_ssl_verify, _custom_provider_entry_to_provider_config,
_entries_for_route, _normalize_custom_provider_entry, _normalize_provider_models,
_pick_provider_base_url, _route_model_cfg, _warn_once_per_provider,
apply_custom_provider_extra_headers_to_client_kwargs,
apply_custom_provider_tls_to_client_kwargs, coerce_provider_id, find_provider_entry,
get_compatible_custom_providers, get_custom_provider_context_length,
get_custom_provider_extra_headers, get_custom_provider_model_capability,
get_custom_provider_tls_settings, is_provider_enabled, normalize_extra_headers,
providers_dict_to_custom_providers, stringify_provider_map)
# Back-compat re-exports — :mod:`hermes_cli.personality` owns personality/overlay semantics.
from hermes_cli.personality import ( # noqa: E402,F401
NEUTRAL_PERSONALITY_NAMES as _NEUTRAL_PERSONALITY_NAMES,
prompt_text as _prompt_text,
render_personality_prompt,
resolve_ephemeral_system_prompt as resolve_ephemeral_system_prompt_from_config)
# ---- Config Migration System ----
+1 -1
View File
@@ -109,7 +109,7 @@ def _lower_is(word: str) -> Callable[[Any], bool]:
def _migrate_to_12(results: Dict[str, Any], quiet: bool) -> None:
# 11 → 12: custom_providers list → providers dict.
from hermes_cli.config_providers import _custom_provider_entry_to_provider_config
_custom_provider_entry_to_provider_config = _cfg()._custom_provider_entry_to_provider_config
config = read_raw_config()
custom_list = config.get("custom_providers")
+3 -1
View File
@@ -357,6 +357,7 @@ def _entries_for_route(
None *custom_providers* → ``get_compatible_custom_providers(config)`` (failure → none).
"""
from hermes_cli.config import get_compatible_custom_providers
if custom_providers is None:
try:
custom_providers = get_compatible_custom_providers(config)
@@ -486,6 +487,7 @@ def get_custom_provider_context_length(
Before this helper existed, the lookup was duplicated in ``run_agent.py``'s startup path only; every
other path (notably ``/model`` switch) fell back to the 128K default. See #15779.
"""
from hermes_cli.config import get_compatible_custom_providers
if not model or not base_url:
return None
if custom_providers is None:
@@ -518,7 +520,7 @@ def get_custom_provider_model_capability(
config: Optional[Dict[str, Any]] = None) -> Optional[bool]:
"""Explicit boolean capability for one custom-provider model, or ``None``. Scoped to the
normalized route + exact runtime model id so aliases can declare capabilities."""
from hermes_cli.config import load_config_readonly
from hermes_cli.config import get_compatible_custom_providers, load_config_readonly
if not model or not base_url or not capability:
return None
if custom_providers is None:
+2 -2
View File
@@ -171,13 +171,13 @@ def _known_provider_ids(cfg: dict) -> tuple[set, list, object, object, object]:
from hermes_cli.auth import PROVIDER_REGISTRY, resolve_provider as resolve_auth
known = set(PROVIDER_REGISTRY.keys()) | {"openrouter", "custom", "auto", "moa"}
with warn_on_error(""):
from hermes_cli.config_providers import get_compatible_custom_providers
from hermes_cli.config import get_compatible_custom_providers
from hermes_cli.providers import custom_provider_aliases as aliases, normalize_provider as normalize, resolve_provider_full as resolve_full
with warn_on_error(""):
custom_providers = get_compatible_custom_providers(cfg)
user_providers = cfg.get("providers")
if isinstance(user_providers, dict):
from hermes_cli.config_providers import is_provider_enabled
from hermes_cli.config import is_provider_enabled
known.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 aliases is not None else ():
+2 -3
View File
@@ -38,9 +38,8 @@ class ConfigContext:
def load_picker_context() -> ConfigContext:
"""Load the disk-config snapshot every consumer needs."""
from hermes_cli.config import load_config
from hermes_cli.config_providers import (
coerce_provider_id, get_compatible_custom_providers, stringify_provider_map
from hermes_cli.config import (
coerce_provider_id, get_compatible_custom_providers, load_config, stringify_provider_map,
)
cfg = load_config()
model_cfg = cfg.get("model", {})
+2 -2
View File
@@ -1,5 +1,5 @@
"""``hermes kanban …`` — dispatch (``kanban_command``), task-verb handlers, ``run_slash`` for ``/kanban``.
DB work lives in ``kanban_db``; siblings: ``kanban_parser`` (argparse ``build_parser``),
DB work lives in ``kanban_db``; siblings: ``kanban_parser`` (argparse, re-exported ``build_parser``),
``kanban_output`` (text/--json), ``kanban_boards`` (``boards …``), ``kanban_ops`` (dispatch/daemon/
tail/watch/gc/repair).
"""
@@ -31,7 +31,7 @@ from hermes_cli.kanban_boards import _dispatch_boards
from hermes_cli.kanban_ops import (
_cmd_daemon, _kanban_config, _cmd_dispatch, _cmd_gc, _cmd_repair, _cmd_tail, _cmd_watch,
)
from hermes_cli.kanban_parser import build_parser
from hermes_cli.kanban_parser import build_parser # noqa: F401 (re-exported: hermes_cli.main, run_slash)
# --- Flag parsing helpers ---
+1 -2
View File
@@ -1999,8 +1999,7 @@ def _resolve_active_provider(config, model_cfg, effective_provider, custom_provi
detection. Unknown/unauthenticated providers warn and fall back to auto.
"""
from hermes_cli.auth import AuthError, format_auth_error, resolve_provider
from hermes_cli.config import get_env_value
from hermes_cli.config_providers import get_compatible_custom_providers
from hermes_cli.config import get_compatible_custom_providers, get_env_value
from hermes_cli.providers import custom_provider_aliases, resolve_provider_full
active = ""
+1 -2
View File
@@ -709,8 +709,7 @@ def _run_anthropic_oauth_flow(save_env_value):
def _named_custom_provider_map(cfg) -> dict[str, dict[str, str]]:
"""Saved custom providers keyed by slug, with raw ``${ENV}`` refs preserved."""
from hermes_cli.config import read_raw_config
from hermes_cli.config_providers import get_compatible_custom_providers
from hermes_cli.config import get_compatible_custom_providers, read_raw_config
from hermes_cli.providers import custom_provider_slug
# Raw (un-expanded) templates keyed by identity. ``get_compatible_custom_providers(
+1 -1
View File
@@ -206,7 +206,7 @@ def _configured_model_ids(cfg_models) -> list[str]:
def _discover_named_custom_models(provider_info: dict, api_key: str, configured_models: list, explicit_catalog: bool):
"""Live catalog probe for a named custom endpoint (native ``/api/tags`` for Ollama).
Returns ``(models, native_catalog_empty)``; persists the live catalog as a side effect."""
from hermes_cli.config_providers import normalize_extra_headers
from hermes_cli.config import normalize_extra_headers
from hermes_cli.models import fetch_api_models, _get_ollama_native_headers
from hermes_cli.models_local import (
fetch_ollama_local_models,
+3 -3
View File
@@ -956,7 +956,7 @@ def _config_declares_model(
when the remote /v1/models does not list it (cloud/aliased models). Custom entries match by
slug alias or by base_url."""
if user_providers:
from hermes_cli.config_providers import is_provider_enabled
from hermes_cli.config import is_provider_enabled
cfg = user_providers.get(target_provider)
if cfg is not None and is_provider_enabled(cfg) and new_model in _declared_model_ids(cfg.get("models", {})):
return True
@@ -1436,7 +1436,7 @@ def _build_switch_result(st: _Switch) -> ModelSwitchResult:
# chat_template_kwargs) so the gateway applies them like the default-provider path does.
request_overrides = None
try:
from hermes_cli.runtime_provider_custom import _get_named_custom_provider, _custom_provider_request_overrides
from hermes_cli.runtime_provider import _get_named_custom_provider, _custom_provider_request_overrides
cp_for_ro = _get_named_custom_provider(st.target_provider)
request_overrides = _custom_provider_request_overrides(cp_for_ro) or None if cp_for_ro else None
except Exception:
@@ -1477,7 +1477,7 @@ def switch_model(
def _extra_headers_from_config(entry: Any) -> dict[str, str]:
if not isinstance(entry, dict):
return {}
from hermes_cli.config_providers import normalize_extra_headers
from hermes_cli.config import normalize_extra_headers
return normalize_extra_headers(entry.get("extra_headers"))
+4 -4
View File
@@ -826,7 +826,7 @@ def _lap_user_provider_rows(b: _PickerBuild, user_providers: dict) -> None:
one row (two Palantir Claude entries -> one "Palantir Claude" row); a different
key_env/api_mode/headers keeps distinct rows since the wire protocol or tenant differs."""
from hermes_cli.model_switch import _extra_headers_from_config, _scoped_key_env
from hermes_cli.config_providers import coerce_provider_id, is_provider_enabled
from hermes_cli.config import coerce_provider_id, is_provider_enabled
ep_groups: dict[tuple, dict] = {}
for ep_name, ep_cfg in user_providers.items():
if not isinstance(ep_cfg, dict) or not is_provider_enabled(ep_cfg) or ep_name.lower() in b.seen_slugs:
@@ -912,7 +912,7 @@ def _lap_custom_provider_rows(b: _PickerBuild, custom_providers: list) -> None:
entries on one host become one "Ollama" row; distinct prefixes sharing a proxy URL keep
their own rows."""
from hermes_cli.model_switch import _extra_headers_from_config, _scoped_key_env
from hermes_cli.config_providers import coerce_provider_id
from hermes_cli.config import coerce_provider_id
groups: dict[tuple, dict] = {}
for entry in custom_providers:
if not isinstance(entry, dict):
@@ -1034,7 +1034,7 @@ def list_authenticated_providers(
``probe_custom_providers`` enables live ``/models`` discovery for saved custom endpoints (CLI
true, GUI false); ``probe_current_custom_provider`` probes only the selected custom endpoint."""
from agent.models_dev import fetch_models_dev
from hermes_cli.config_providers import coerce_provider_id, stringify_provider_map
from hermes_cli.config import coerce_provider_id, stringify_provider_map
# Explicit refresh: drop every cached list so the calls below re-fetch live. A stale cache
# can fall back to the curated static list when its live fetch fails, silently dropping
@@ -1089,7 +1089,7 @@ def _finalize_picker_rows(results: list, user_providers, current_model: str) ->
# The enabled post-filter covers built-in rows (sections 1-2) that bypass the per-section
# gate; matched by slug and ``provider_id``.
try:
from hermes_cli.config_providers import is_provider_enabled
from hermes_cli.config import is_provider_enabled
if isinstance(user_providers, dict):
disabled = {
str(name).strip().lower() for name, cfg in user_providers.items()
+2 -2
View File
@@ -124,7 +124,7 @@ def _custom_provider_ssl_context(base_url: str):
if not base_url:
return None
try:
from hermes_cli.config_providers import get_custom_provider_tls_settings
from hermes_cli.config import get_custom_provider_tls_settings
tls = get_custom_provider_tls_settings(base_url)
if not tls:
@@ -2138,7 +2138,7 @@ def probe_api_models(
headers.update(copilot_default_headers())
if isinstance(request_headers, dict):
# Per-provider custom headers can contain secrets: merge last so endpoint config wins; never log.
from hermes_cli.config_providers import normalize_extra_headers
from hermes_cli.config import normalize_extra_headers
headers.update(normalize_extra_headers(request_headers))
+1 -1
View File
@@ -135,7 +135,7 @@ def _get_ollama_request_headers() -> dict[str, str]:
from hermes_cli.models import _get_provider_config_dict
entry = _get_provider_config_dict("ollama")
try:
from hermes_cli.config_providers import normalize_extra_headers
from hermes_cli.config import normalize_extra_headers
result = normalize_extra_headers(entry.get("extra_headers"))
except (ImportError, OSError, RuntimeError, TypeError, ValueError):
+2 -2
View File
@@ -223,7 +223,7 @@ def _check_manifest_v2(report: "DoctorReport", manifest: Any) -> None:
"""Manifest v2 checks: versions, deps, pip declarations, config schema."""
import importlib.metadata
import re as _re
from hermes_cli.plugins_manifest import SUPPORTED_MANIFEST_VERSION
from hermes_cli.plugins import SUPPORTED_MANIFEST_VERSION
mv = getattr(manifest, "manifest_version", 1)
if mv > SUPPORTED_MANIFEST_VERSION:
report.warning(
@@ -272,7 +272,7 @@ def _check_manifest_v2(report: "DoctorReport", manifest: Any) -> None:
schema = getattr(manifest, "config_schema", {}) or {}
if schema:
from hermes_cli.plugins_manifest import _CONFIG_SCHEMA_TYPES
from hermes_cli.plugins import _CONFIG_SCHEMA_TYPES
for skey, spec in schema.items():
stype = spec.get("type") if isinstance(spec, dict) else None
if stype is not None and str(stype).lower() not in _CONFIG_SCHEMA_TYPES:
+36 -9
View File
@@ -35,21 +35,27 @@ from hermes_cli.middleware import VALID_MIDDLEWARE
from hermes_cli.plugin_capabilities import plugin_capability_granted
from hermes_cli.relay_plugin_cutover import RELAY_PLUGINS_CONFIG_ENV, legacy_relay_plugin_keys
# Sibling modules' names are re-exported here (origin) so plugins and tests keep one import path.
from hermes_cli.plugins_manifest import PluginManifest, manifest_key, resolve_plugin_load_order
from hermes_cli.plugins_discovery import (
_get_disabled_plugins, _get_enabled_plugins, collect_directory_manifests, discover_entrypoint_manifests,
gate_manifest, scan_directory
from hermes_cli.plugins_manifest import ( # noqa: F401 — re-exported
_CONFIG_SCHEMA_TYPES, SUPPORTED_MANIFEST_VERSION, PluginManifest, _portable_skill_namespace,
manifest_key, parse_manifest_file, resolve_module_origin, resolve_plugin_load_order,
validate_config_schema,
)
from hermes_cli.plugins_discovery import ( # noqa: F401 — re-exported
ENTRY_POINTS_GROUP, _get_disabled_plugins, _get_enabled_plugins, collect_directory_manifests,
discover_entrypoint_manifests, gate_manifest, scan_directory,
)
from hermes_cli.plugins_loader import (
PluginLoaderMixin, _BARE_MODULE_SCOPE, _MODULE_NAMESPACE_LOCK, _NS_PARENT, _evict_modules,
_plugin_home_scope, _serialized_replacement,
)
from hermes_cli.plugins_dispatch import (
from hermes_cli.plugins_dispatch import ( # noqa: F401 — re-exported
DEFAULT_SYSTEM_PROMPT_SECTION_MAX_CHARS, HERMES_EVENT_NAMESPACE, MAX_SYSTEM_PROMPT_SECTION_CHARS,
SYSTEM_PROMPT_SECTION_POSITIONS, _EVENT_PENDING_CAP, _HOOK_CALLBACK_TIMEOUT_SECS,
_HOOK_TIMEOUT_SUPPRESSION_SECONDS, _MAX_HOOK_CALLBACK_TIMEOUT_SECS, PluginDispatchMixin,
PluginSystemPromptSection, RenderedPluginSystemPromptSection, _EventSubscription,
is_valid_system_prompt_section_id
MAX_SYSTEM_PROMPT_SECTIONS_TOTAL_CHARS, PLUGIN_SECTIONS_END, PLUGIN_SECTIONS_START,
SYSTEM_PROMPT_SECTION_POSITIONS, _EVENT_EMIT_DEPTH_CAP, _EVENT_PENDING_CAP,
_HOOK_CALLBACK_TIMEOUT_SECS, _HOOK_TIMEOUT_SUPPRESSION_SECONDS, _MAX_HOOK_CALLBACK_TIMEOUT_SECS,
_PRE_TOOL_CALL_TIMEOUT_BLOCK_MESSAGE, PluginDispatchMixin, PluginSystemPromptSection,
RenderedPluginSystemPromptSection, _EventSubscription, format_system_prompt_sections,
is_valid_system_prompt_section_id,
)
from hermes_cli.plugins_ledger import PluginLedgerMixin, PluginRegistration
from hermes_cli.plugins_state import (
@@ -1400,6 +1406,10 @@ class PluginManager(PluginLoaderMixin, PluginDispatchMixin, PluginLedgerMixin):
adapter)`` at connect (see :meth:`PluginContext.register_platform_handler`)."""
return list(self._platform_handler_factories.get((platform or "").strip().lower(), []))
def get_telegram_handler_factories(self) -> List[tuple]:
"""Back-compat alias for ``get_platform_handler_factories("telegram")``."""
return self.get_platform_handler_factories("telegram")
def list_plugins(self) -> List[Dict[str, Any]]:
"""Return a list of info dicts for all discovered plugins."""
return [
@@ -1788,6 +1798,23 @@ def _get_pre_tool_call_directive_details(
return _PreToolCallDirective(modified_args=modified_args)
def get_pre_tool_call_directive(
tool_name: str, args: Optional[Dict[str, Any]], **hook_kwargs: Any
) -> tuple[Optional[str], Optional[str]]:
"""Back-compat: ``(directive, message)`` with directive ``"block"`` / ``"approve"`` / ``None``.
``hook_kwargs`` are the observability ids of :func:`_get_pre_tool_call_directive_details`."""
details = _get_pre_tool_call_directive_details(tool_name, args, **hook_kwargs)
return (details.action, details.message)
def get_pre_tool_call_block_message(
tool_name: str, args: Optional[Dict[str, Any]], **hook_kwargs: Any
) -> Optional[str]:
"""Deprecated shim: only the ``block`` message (or ``None``); ``approve`` is invisible here."""
directive, message = get_pre_tool_call_directive(tool_name, args, **hook_kwargs)
return message if directive == "block" else None
def resolve_pre_tool_block(
tool_name: str, args: Optional[Dict[str, Any]], **hook_kwargs: Any
) -> Optional[str]:
+22 -13
View File
@@ -12,19 +12,22 @@ import re
from dataclasses import dataclass
from typing import Any, Callable, Dict, Optional
from urllib.parse import urlparse
from hermes_cli import config_providers
logger = logging.getLogger(__name__)
from hermes_cli import auth as auth_mod
from agent.credential_pool import CredentialPool, PooledCredential, credential_pool_matches_provider, load_pool
from agent.credential_pool import ( # custom_provider_pool_key_candidates is read via origin by runtime_provider_custom
CredentialPool, PooledCredential, credential_pool_matches_provider, custom_provider_pool_key_candidates, # noqa: F401
load_pool,
)
from agent.secret_scope import get_secret as _get_secret
from hermes_cli.auth import (
ACTUAL_LOCAL_NOAUTH_PLACEHOLDER, AuthError, DEFAULT_CODEX_BASE_URL, DEFAULT_QWEN_BASE_URL,
DEFAULT_XAI_OAUTH_BASE_URL, PROVIDER_REGISTRY, _agent_key_is_usable, _nous_inference_env_override,
format_auth_error, resolve_provider, resolve_nous_runtime_credentials, resolve_codex_runtime_credentials,
resolve_xai_oauth_runtime_credentials, resolve_qwen_runtime_credentials, resolve_api_key_provider_credentials,
has_usable_secret, is_actual_local_base_url, normalize_actual_base_url
from hermes_cli.auth import ( # resolve_external_process_provider_credentials is read via origin by runtime_provider_backends
ACTUAL_LOCAL_NOAUTH_PLACEHOLDER, AuthError, DEFAULT_CODEX_BASE_URL, DEFAULT_QWEN_BASE_URL, DEFAULT_XAI_OAUTH_BASE_URL,
PROVIDER_REGISTRY, _agent_key_is_usable, _nous_inference_env_override, format_auth_error, resolve_provider,
resolve_nous_runtime_credentials, resolve_codex_runtime_credentials, resolve_xai_oauth_runtime_credentials,
resolve_qwen_runtime_credentials, resolve_api_key_provider_credentials,
resolve_external_process_provider_credentials, # noqa: F401
has_usable_secret, is_actual_local_base_url, normalize_actual_base_url,
)
from hermes_cli import config as _config_mod
from hermes_cli import models as _models # attribute access keeps ``hermes_cli.models.<name>`` patches effective
@@ -41,11 +44,11 @@ def load_config():
def get_compatible_custom_providers(config=None):
return config_providers.get_compatible_custom_providers(config)
return _config_mod.get_compatible_custom_providers(config)
def normalize_extra_headers(value):
return config_providers.normalize_extra_headers(value)
return _config_mod.normalize_extra_headers(value)
def _getenv(name: str, default: str = "") -> str:
@@ -132,7 +135,7 @@ def _parse_api_mode(raw: Any) -> Optional[str]:
"""Validate an api_mode from config (None if invalid). Legacy/alias spellings (``openai``,
``anthropic``, ``responses``, …) are canonicalized first so old configs keep their transport
instead of silently falling through to hostname-based detection."""
normalized = config_providers._canonical_api_mode(raw).lower() if isinstance(raw, str) else ""
normalized = _config_mod._canonical_api_mode(raw).lower() if isinstance(raw, str) else ""
return normalized if normalized in _VALID_API_MODES else None
@@ -399,7 +402,13 @@ def resolve_requested_provider(requested: Optional[str] = None) -> str:
# ── extracted collaborators (re-exported; see module docstring) ────────────────────────────
from hermes_cli.runtime_provider_custom import _resolve_named_custom_runtime # noqa: E402
from hermes_cli.runtime_provider_custom import ( # noqa: E402,F401
_apply_custom_provider_extras, _custom_provider_request_overrides, _filter_capabilities, _find_custom_identity,
_get_named_custom_provider, _lift_common_custom_fields, _lift_extra_headers, _lift_max_output_tokens,
_lift_model_capabilities, _normalize_base_url_for_match, _normalize_custom_provider_name, _resolve_named_custom_runtime,
_try_resolve_from_custom_pool, canonical_custom_identity, find_custom_provider_identity,
find_custom_provider_identity_by_model, has_named_custom_provider, is_routable_provider,
)
from hermes_cli.runtime_provider_backends import ( # noqa: E402,F401
_is_external_process_provider, _resolve_azure_foundry_runtime, _resolve_bedrock_runtime,
_resolve_external_process_runtime, _resolve_openrouter_runtime,
@@ -724,7 +733,7 @@ def _raise_if_provider_disabled(requested_provider: str) -> None:
full_cfg = _config_mod.load_config()
provs_cfg = full_cfg.get("providers") if isinstance(full_cfg, dict) else None
block = provs_cfg.get(requested_provider) if isinstance(provs_cfg, dict) else None
if isinstance(block, dict) and not config_providers.is_provider_enabled(block):
if isinstance(block, dict) and not _config_mod.is_provider_enabled(block):
raise ValueError(f"provider {requested_provider!r} is disabled in config "
f"(providers.{requested_provider}.enabled: false)")
+2 -4
View File
@@ -157,8 +157,7 @@ def _resolve_openrouter_runtime(
return rp._runtime("openrouter", cfg_api_mode or rp._detect_api_mode_for_url(base_url) or "chat_completions", base_url,
api_key, source=source)
if base_url:
from hermes_cli.runtime_provider_custom import _try_resolve_from_custom_pool
pool_result = _try_resolve_from_custom_pool(base_url, "custom", cfg_api_mode, provider_name=None)
pool_result = rp._try_resolve_from_custom_pool(base_url, "custom", cfg_api_mode, provider_name=None)
if pool_result:
return pool_result
# Local no-auth servers get a placeholder key — the OpenAI SDK requires a non-empty string.
@@ -249,8 +248,7 @@ def _is_external_process_provider(provider: str) -> bool:
def _resolve_external_process_runtime(provider: str, requested_provider: str) -> Dict[str, Any]:
rp = _rp()
from hermes_cli.auth import resolve_external_process_provider_credentials
creds = resolve_external_process_provider_credentials(provider)
creds = rp.resolve_external_process_provider_credentials(provider)
return rp._runtime(provider, "chat_completions", creds.get("base_url", "").rstrip("/"), creds.get("api_key", ""),
command=creds.get("command", ""), args=list(creds.get("args") or []),
source=creds.get("source", "process"), requested_provider=requested_provider)
+7 -8
View File
@@ -119,7 +119,7 @@ def _shadowed_by_builtin(requested_norm: str) -> bool:
def _match_new_style_provider(requested_norm: str, providers: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""Scan ``providers:`` (new-style, keyed) for ``requested_norm``."""
from hermes_cli.config_providers import is_provider_enabled
from hermes_cli.config import is_provider_enabled
rp = _rp()
for ep_name, entry in providers.items():
# ``providers.<name>.enabled: false`` entries stay in config but are invisible here.
@@ -192,7 +192,7 @@ def has_named_custom_provider(requested_provider: str) -> bool:
"""True when config defines a ``providers:`` / ``custom_providers:`` entry matching the request
(public wrapper so e.g. the cronjob tool need not reach into a private helper)."""
try:
return _get_named_custom_provider(requested_provider) is not None
return _rp()._get_named_custom_provider(requested_provider) is not None
except Exception:
return False
@@ -291,7 +291,7 @@ def canonical_custom_identity(*, base_url: Optional[str] = None, config_provider
# keyed ``providers:`` entry — re-resolve via its endpoint so every path returns the same
# config-key slug.
try:
entry = _get_named_custom_provider(candidate)
entry = rp._get_named_custom_provider(candidate)
except Exception:
return None
if entry is None:
@@ -331,9 +331,8 @@ def _try_resolve_from_custom_pool(
) -> Optional[Dict[str, Any]]:
"""Runtime dict from the first credential pool that owns this custom endpoint, else None."""
rp = _rp()
from agent.credential_pool import custom_provider_pool_key_candidates
try:
raw_keys = list(custom_provider_pool_key_candidates(base_url, provider_name))
raw_keys = list(rp.custom_provider_pool_key_candidates(base_url, provider_name))
except Exception:
raw_keys = []
# Order-preserving dedupe of normalized keys.
@@ -427,7 +426,7 @@ def _resolve_direct_alias_runtime(requested_provider: str, explicit_api_key: Opt
base_url = explicit_base_url.strip().rstrip("/")
# Pool first — mirrors the named-custom path so bare `provider: custom` with a configured
# custom_providers entry gets its api_key from the pool instead of env fallbacks.
pool_result = _try_resolve_from_custom_pool(base_url, "custom", None)
pool_result = rp._try_resolve_from_custom_pool(base_url, "custom", None)
if pool_result:
pool_result["source"] = "direct-alias"
return pool_result
@@ -476,13 +475,13 @@ def _resolve_named_custom_runtime(*, requested_provider: str, explicit_api_key:
requested_norm = "custom"
if requested_norm == "custom" and explicit_base_url:
return _resolve_direct_alias_runtime(requested_provider, explicit_api_key, explicit_base_url)
custom_provider = _get_named_custom_provider(requested_provider)
custom_provider = rp._get_named_custom_provider(requested_provider)
if not custom_provider:
return None
base_url = ((explicit_base_url or "").strip() or custom_provider.get("base_url", "")).rstrip("/")
if not base_url:
return None
pool_result = _try_resolve_from_custom_pool(
pool_result = rp._try_resolve_from_custom_pool(
base_url, "custom", custom_provider.get("api_mode"),
provider_name=custom_provider.get("provider_key") or custom_provider.get("name"),
)
+14
View File
@@ -52,6 +52,20 @@ def _load_bw():
return _bw
def __getattr__(name: str):
"""PEP 562 lazy ``bw`` attribute (tests monkeypatch ``secrets_cli.bw``); an eager binding
would re-import ``cryptography`` at import time.
Existing callers (and upstream tests) monkeypatch attributes on ``hermes_cli.secrets_cli.bw`` directly.
Resolving that attribute at module-import time would re-import ``cryptography`` eagerly — the very
self-lock we are preventing (#86781). Defer the backend import until the first actual attribute access,
so ``import hermes_cli.secrets_cli`` stays crypto-free while ``secrets_cli.bw.find_bws`` still resolves.
"""
if name == "bw":
return _load_bw()
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
# ── Argparse wiring — called from hermes_cli.main ──
+2 -2
View File
@@ -18,7 +18,7 @@ def _build_full_manifest(
Other sections (OAuth scopes, socket mode) are sensible Hermes defaults, tweakable in the Slack
UI after pasting.
"""
from hermes_cli.commands_platforms import slack_app_manifest
from hermes_cli.commands import slack_app_manifest
if messaging_experience is None:
messaging_experience = "assistant" if include_assistant else "none"
messaging_experience = str(messaging_experience).strip().lower()
@@ -113,7 +113,7 @@ def slack_manifest_command(args) -> int:
messaging_experience = "assistant"
if slashes_only:
from hermes_cli.commands_platforms import slack_app_manifest
from hermes_cli.commands import slack_app_manifest
manifest = slack_app_manifest()["features"]["slash_commands"]
else:
manifest = _build_full_manifest(
+1 -4
View File
@@ -14,10 +14,7 @@ from fastapi import APIRouter
from hermes_cli.web_routers._common import http_failure, scoped_to_thread
from hermes_cli.web_deps import LateState, late
from fastapi import HTTPException, Request
from hermes_cli.config import (
DEFAULT_CONFIG, OPTIONAL_ENV_VARS, read_raw_config, custom_endpoint_key_env, redact_key, _deep_merge
)
from hermes_cli.config_providers import coerce_provider_id, find_provider_entry
from hermes_cli.config import DEFAULT_CONFIG, OPTIONAL_ENV_VARS, read_raw_config, custom_endpoint_key_env, coerce_provider_id, find_provider_entry, redact_key, _deep_merge
from hermes_cli.web_models import ConfigUpdate, EnvVarUpdate, EnvVarDelete, EnvVarReveal, CustomEndpointUpdate
from typing import Any, Dict, List, Optional, Tuple
+8 -4
View File
@@ -10,10 +10,14 @@ import os
from fastapi import HTTPException
from typing import Any, Dict, List, Optional, Tuple
from hermes_cli.config import (
DEFAULT_CONFIG, build_cron_model_impact, cfg_get, clear_model_endpoint_credentials, read_raw_config,
resolve_cron_model_drift_defaults
DEFAULT_CONFIG,
build_cron_model_impact,
cfg_get,
clear_model_endpoint_credentials,
find_provider_entry,
read_raw_config,
resolve_cron_model_drift_defaults,
)
from hermes_cli.config_providers import find_provider_entry
from hermes_cli.web_server_memory import _normalize_memory_provider_name
# Same logger the code used before extraction (record parity).
@@ -390,7 +394,7 @@ def _normalize_main_model_assignment(provider: str, model: str) -> tuple[str, st
``normalize_model_for_provider`` (custom/user providers keep the model verbatim).
"""
from hermes_cli.config import load_config
from hermes_cli.config_providers import get_compatible_custom_providers
from hermes_cli.config import get_compatible_custom_providers
from hermes_cli.models import _AGGREGATOR_PROVIDERS, _KNOWN_PROVIDER_NAMES, normalize_provider
from hermes_cli.model_normalize import normalize_model_for_provider
from hermes_cli.providers import resolve_custom_provider, resolve_user_provider
+1 -2
View File
@@ -18,8 +18,7 @@ from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, Response
from fastapi.staticfiles import StaticFiles
from pathlib import Path
from typing import Any, Dict, List, Optional
from hermes_cli.config import cfg_get
from hermes_constants import get_process_hermes_home
from hermes_cli.config import cfg_get, get_process_hermes_home
from utils import env_var_enabled
# Same logger the code used before extraction (record parity).
+1 -2
View File
@@ -16,8 +16,7 @@ from contextlib import contextmanager
from fastapi import HTTPException
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional
from hermes_cli.config import DEFAULT_CONFIG
from hermes_constants import get_process_hermes_home
from hermes_cli.config import DEFAULT_CONFIG, get_process_hermes_home
from hermes_cli.web_models import MCPServerCreate
from hermes_cli.web_server_gateway import _ACTION_LOG_FILES
from hermes_cli.web_server_mcp import _normalize_mcp_server_create
+1 -1
View File
@@ -127,7 +127,7 @@ def _entry_point_package_dir(entry_point) -> Optional[Path]:
if entry_point is None:
return None
try:
from hermes_cli.plugins_manifest import resolve_module_origin
from hermes_cli.plugins import resolve_module_origin
module_name = (entry_point.value or "").split(":")[0].strip()
origin = resolve_module_origin(module_name)
+1 -1
View File
@@ -4671,7 +4671,7 @@ class DiscordAdapter(BasePlatformAdapter):
def _refresh_skill_catalog_state(self) -> None:
"""Re-scan disk and repopulate ``self._skill_entries``/``_skill_lookup`` in place.
No Discord API calls: autocomplete and handler read these attributes directly."""
from hermes_cli.commands_platforms import discord_skill_commands_by_category
from hermes_cli.commands import discord_skill_commands_by_category
reserved = getattr(self, "_skill_group_reserved_names", set())
categories, uncategorized, hidden = discord_skill_commands_by_category(
reserved_names=set(reserved),
+2 -2
View File
@@ -1479,7 +1479,7 @@ class SlackAdapter(BasePlatformAdapter):
# Every COMMAND_REGISTRY command is a native slash via one regex matcher. Commands must
# ALSO be declared in the app manifest (`hermes slack manifest`): Socket Mode won't
# deliver undeclared commands at all.
from hermes_cli.commands_platforms import slack_native_slashes
from hermes_cli.commands import slack_native_slashes
_slash_names = [name for name, _d, _h in slack_native_slashes()]
if _slash_names:
_slash_pattern = re.compile(
@@ -5257,7 +5257,7 @@ class SlackAdapter(BasePlatformAdapter):
if slash_name not in {"hermes", ""}:
return f"/{slash_name}" if not raw_text else f"/{slash_name} {raw_text}"
legacy_text = raw_text.strip()
from hermes_cli.commands_platforms import slack_subcommand_map
from hermes_cli.commands import slack_subcommand_map
subcommand_map = slack_subcommand_map()
subcommand_map["compact"] = "/compress"
first_word = legacy_text.split()[0] if legacy_text.split() else ""
+2 -2
View File
@@ -2522,7 +2522,7 @@ class TelegramAdapter(BasePlatformAdapter):
"""Register the command menu (from COMMAND_REGISTRY) in every scope — Telegram picks the
narrowest matching one per chat type; forum topics are handled lazily by _ensure_forum_commands."""
from telegram import BotCommand, BotCommandScopeAllPrivateChats, BotCommandScopeAllGroupChats, BotCommandScopeDefault
from hermes_cli.commands_platforms import telegram_menu_commands, telegram_menu_max_commands
from hermes_cli.commands import telegram_menu_commands, telegram_menu_max_commands
if not self._bot:
return
# Telegram allows 100 commands but has an undocumented ~4KB payload limit; default cap 60.
@@ -5630,7 +5630,7 @@ class TelegramAdapter(BasePlatformAdapter):
if chat_id in self._forum_command_registered:
return
from telegram import BotCommand, BotCommandScopeChat
from hermes_cli.commands_platforms import telegram_menu_commands, telegram_menu_max_commands
from hermes_cli.commands import telegram_menu_commands, telegram_menu_max_commands
menu_commands, _ = telegram_menu_commands(max_commands=telegram_menu_max_commands())
bot_commands = [BotCommand(name, desc) for name, desc in menu_commands]
await self._bot.set_my_commands(bot_commands, scope=BotCommandScopeChat(chat_id=chat_id))
+1 -3
View File
@@ -33,9 +33,7 @@ def collect_inline_catalog() -> List[Dict[str, str]]:
catalog.append({"name": name, "description": desc or ""})
try:
from hermes_cli.commands_platforms import (
_collect_gateway_skill_entries, _sanitize_telegram_name, telegram_bot_commands
)
from hermes_cli.commands import _collect_gateway_skill_entries, _sanitize_telegram_name, telegram_bot_commands
except Exception: # pragma: no cover - defensive
logger.debug("inline picker: commands registry unavailable", exc_info=True)
return catalog
@@ -15,7 +15,6 @@ import httpx
import pytest
from agent.process_bootstrap import build_keepalive_http_client
from hermes_cli import config_providers
_CA_ENV_VARS = ("HERMES_CA_BUNDLE", "SSL_CERT_FILE", "REQUESTS_CA_BUNDLE", "HTTPS_PROXY")
@@ -44,7 +43,7 @@ def test_resolve_aux_verify_ssl_verify_false(clean_tls_env, monkeypatch):
from agent import auxiliary_client
monkeypatch.setattr(
config_providers,
cfg,
"get_custom_provider_tls_settings",
lambda *a, **k: {"ssl_verify": False},
)
@@ -264,7 +264,7 @@ class TestProvidersDictApiModeAnthropicMessages:
},
},
})
from hermes_cli.runtime_provider_custom import _get_named_custom_provider
from hermes_cli.runtime_provider import _get_named_custom_provider
entry = _get_named_custom_provider("myrelay")
assert entry is not None
assert entry.get("api_mode") == "anthropic_messages"
+1 -2
View File
@@ -25,7 +25,6 @@ from agent.command_token_source import (
_mint,
build_command_token_provider,
)
from hermes_cli import runtime_provider_custom
class TestMinting:
@@ -312,7 +311,7 @@ class TestAuxiliaryResolverHonoursKeyCmd:
from hermes_cli import runtime_provider as rp
monkeypatch.setattr(
runtime_provider_custom, "_get_named_custom_provider",
rp, "_get_named_custom_provider",
lambda name: dict(entry, name="dbx") if name == "dbx" else None,
)
seen = {}
+18 -18
View File
@@ -79,7 +79,7 @@ class TestResolveRequestsVerifyProviderScoped:
def test_provider_ca_used_for_matching_base_url(self, clean_env, bundle_file):
with patch(
"hermes_cli.config_providers.get_compatible_custom_providers",
"hermes_cli.config.get_compatible_custom_providers",
return_value=_providers(_BASE, ssl_ca_cert=bundle_file),
):
assert _resolve_requests_verify(_BASE) == bundle_file
@@ -89,14 +89,14 @@ class TestResolveRequestsVerifyProviderScoped:
env_bundle.write_text("stub")
clean_env.setenv("SSL_CERT_FILE", str(env_bundle))
with patch(
"hermes_cli.config_providers.get_compatible_custom_providers",
"hermes_cli.config.get_compatible_custom_providers",
return_value=_providers(_BASE, ssl_ca_cert=bundle_file),
):
assert _resolve_requests_verify(_BASE) == bundle_file
def test_provider_ssl_verify_false_disables(self, clean_env):
with patch(
"hermes_cli.config_providers.get_compatible_custom_providers",
"hermes_cli.config.get_compatible_custom_providers",
return_value=_providers(_BASE, ssl_verify=False),
):
assert _resolve_requests_verify(_BASE) is False
@@ -105,21 +105,21 @@ class TestResolveRequestsVerifyProviderScoped:
"""Existing callers pass no base_url — env-only behavior, no config read."""
clean_env.setenv("HERMES_CA_BUNDLE", bundle_file)
probe = MagicMock(return_value=[])
with patch("hermes_cli.config_providers.get_compatible_custom_providers", probe):
with patch("hermes_cli.config.get_compatible_custom_providers", probe):
assert _resolve_requests_verify() == bundle_file
probe.assert_not_called()
def test_unmatched_base_url_falls_through_to_env(self, clean_env, bundle_file):
clean_env.setenv("REQUESTS_CA_BUNDLE", bundle_file)
with patch(
"hermes_cli.config_providers.get_compatible_custom_providers",
"hermes_cli.config.get_compatible_custom_providers",
return_value=_providers("https://other.example.invalid/v1", ssl_ca_cert="/nope.pem"),
):
assert _resolve_requests_verify(_BASE) == bundle_file
def test_unmatched_base_url_no_env_returns_true(self, clean_env):
with patch(
"hermes_cli.config_providers.get_compatible_custom_providers",
"hermes_cli.config.get_compatible_custom_providers",
return_value=[],
):
assert _resolve_requests_verify(_BASE) is True
@@ -127,7 +127,7 @@ class TestResolveRequestsVerifyProviderScoped:
def test_provider_ca_missing_file_falls_through_to_env(self, clean_env, bundle_file):
clean_env.setenv("SSL_CERT_FILE", bundle_file)
with patch(
"hermes_cli.config_providers.get_compatible_custom_providers",
"hermes_cli.config.get_compatible_custom_providers",
return_value=_providers(_BASE, ssl_ca_cert="/does/not/exist.pem"),
):
assert _resolve_requests_verify(_BASE) == bundle_file
@@ -135,7 +135,7 @@ class TestResolveRequestsVerifyProviderScoped:
def test_config_lookup_failure_falls_through_to_env(self, clean_env, bundle_file):
clean_env.setenv("SSL_CERT_FILE", bundle_file)
with patch(
"hermes_cli.config_providers.get_compatible_custom_providers",
"hermes_cli.config.get_compatible_custom_providers",
side_effect=RuntimeError("config boom"),
):
assert _resolve_requests_verify(_BASE) == bundle_file
@@ -146,7 +146,7 @@ class TestCustomProviderSSLContext:
def test_returns_verifying_context_with_provider_ca(self, real_ca):
with patch(
"hermes_cli.config_providers.get_compatible_custom_providers",
"hermes_cli.config.get_compatible_custom_providers",
return_value=_providers(_BASE, ssl_ca_cert=real_ca),
):
ctx = _custom_provider_ssl_context(_BASE)
@@ -155,7 +155,7 @@ class TestCustomProviderSSLContext:
def test_ssl_verify_false_returns_unverified_context(self):
with patch(
"hermes_cli.config_providers.get_compatible_custom_providers",
"hermes_cli.config.get_compatible_custom_providers",
return_value=_providers(_BASE, ssl_verify=False),
):
ctx = _custom_provider_ssl_context(_BASE)
@@ -168,21 +168,21 @@ class TestCustomProviderSSLContext:
def test_unmatched_returns_none(self):
with patch(
"hermes_cli.config_providers.get_compatible_custom_providers",
"hermes_cli.config.get_compatible_custom_providers",
return_value=[],
):
assert _custom_provider_ssl_context(_BASE) is None
def test_missing_ca_file_returns_none(self):
with patch(
"hermes_cli.config_providers.get_compatible_custom_providers",
"hermes_cli.config.get_compatible_custom_providers",
return_value=_providers(_BASE, ssl_ca_cert="/does/not/exist.pem"),
):
assert _custom_provider_ssl_context(_BASE) is None
def test_config_lookup_failure_returns_none(self):
with patch(
"hermes_cli.config_providers.get_compatible_custom_providers",
"hermes_cli.config.get_compatible_custom_providers",
side_effect=RuntimeError("config boom"),
):
assert _custom_provider_ssl_context(_BASE) is None
@@ -206,7 +206,7 @@ class TestMetadataProbeThreadsProviderCA:
mm._endpoint_model_metadata_cache.clear()
mm._endpoint_model_metadata_cache_time.clear()
with patch(
"hermes_cli.config_providers.get_compatible_custom_providers",
"hermes_cli.config.get_compatible_custom_providers",
return_value=_providers(_BASE, ssl_ca_cert=bundle_file),
), patch.object(mm.requests, "get", side_effect=fake_get):
mm.fetch_endpoint_model_metadata(_BASE, force_refresh=True)
@@ -228,7 +228,7 @@ class TestMetadataProbeThreadsProviderCA:
mm._endpoint_model_metadata_cache.clear()
mm._endpoint_model_metadata_cache_time.clear()
with patch(
"hermes_cli.config_providers.get_compatible_custom_providers",
"hermes_cli.config.get_compatible_custom_providers",
return_value=[],
), patch.object(mm.requests, "get", side_effect=fake_get):
mm.fetch_endpoint_model_metadata(_BASE, force_refresh=True)
@@ -249,7 +249,7 @@ class TestCatalogProbeThreadsSSLContext:
raise urllib.error.URLError("stop after capture")
with patch(
"hermes_cli.config_providers.get_compatible_custom_providers",
"hermes_cli.config.get_compatible_custom_providers",
return_value=_providers(_BASE, ssl_ca_cert=real_ca),
), patch.object(models, "open_credentialed_url", side_effect=fake_open):
models.probe_api_models(None, _BASE, timeout=1)
@@ -267,7 +267,7 @@ class TestCatalogProbeThreadsSSLContext:
raise urllib.error.URLError("stop after capture")
with patch(
"hermes_cli.config_providers.get_compatible_custom_providers",
"hermes_cli.config.get_compatible_custom_providers",
return_value=[],
), patch.object(models, "open_credentialed_url", side_effect=fake_open):
models.probe_api_models(None, _BASE, timeout=1)
@@ -301,7 +301,7 @@ class TestCatalogProbeThreadsSSLContext:
return _Resp()
with patch(
"hermes_cli.config_providers.get_compatible_custom_providers",
"hermes_cli.config.get_compatible_custom_providers",
return_value=[],
), patch.object(
models, "_urlopen_model_catalog_request", side_effect=_strict_two_arg
+1 -1
View File
@@ -68,7 +68,7 @@ class TestPlanRegistryWiring:
assert "plan" in GATEWAY_KNOWN_COMMANDS
def test_plan_in_telegram_bot_commands(self):
from hermes_cli.commands_platforms import telegram_bot_commands
from hermes_cli.commands import telegram_bot_commands
names = {n for n, _ in telegram_bot_commands()}
assert "plan" in names
+1 -1
View File
@@ -197,7 +197,7 @@ def test_provider_flag_logs_when_custom_default_model_cannot_resolve(monkeypatch
raise RuntimeError("catalog unavailable")
monkeypatch.setattr(
"hermes_cli.runtime_provider_custom._get_named_custom_provider",
"hermes_cli.runtime_provider._get_named_custom_provider",
_boom,
)
+3 -4
View File
@@ -12,7 +12,6 @@ import pytest
import cli as cli_mod
from hermes_state import SessionDB
from hermes_cli import runtime_provider_custom
def _make_stub(**overrides):
@@ -232,7 +231,7 @@ def test_persist_model_switch_heals_bare_custom(monkeypatch):
api_mode = ""
import hermes_cli.runtime_provider as rp
monkeypatch.setattr(runtime_provider_custom, "canonical_custom_identity",
monkeypatch.setattr(rp, "canonical_custom_identity",
lambda base_url=None, model=None: "custom:myendpoint")
stub = _make_stub(_session_db=_DB(), session_id="s1")
stub._persist_model_switch_to_session(_BareResult())
@@ -240,7 +239,7 @@ def test_persist_model_switch_heals_bare_custom(monkeypatch):
# Healing fails -> provider dropped (explicit None deletes any stale
# persisted provider), never persisted bare.
monkeypatch.setattr(runtime_provider_custom, "canonical_custom_identity",
monkeypatch.setattr(rp, "canonical_custom_identity",
lambda base_url=None, model=None: None)
written.clear()
stub._persist_model_switch_to_session(_BareResult())
@@ -251,7 +250,7 @@ def test_persist_model_switch_heals_bare_custom(monkeypatch):
def test_restore_session_model_heals_bare_custom_stored_rows(monkeypatch):
"""Rows persisted by older builds may carry bare 'custom' — heal or drop."""
import hermes_cli.runtime_provider as rp
monkeypatch.setattr(runtime_provider_custom, "canonical_custom_identity",
monkeypatch.setattr(rp, "canonical_custom_identity",
lambda base_url=None, model=None: None)
stub = _make_stub()
stub._restore_session_model(_row(model_config={
+1 -1
View File
@@ -193,7 +193,7 @@ class TestBackgroundInCLICommands:
def test_bg_autocompletes(self):
"""The /bg and /btw commands appear in autocomplete results."""
pytest.importorskip("prompt_toolkit")
from hermes_cli.commands_completion import SlashCommandCompleter
from hermes_cli.commands import SlashCommandCompleter
from prompt_toolkit.document import Document
completer = SlashCommandCompleter()
+2 -2
View File
@@ -530,7 +530,7 @@ def test_register_skill_command_callback_dispatches_by_name(adapter):
]
with patch(
"hermes_cli.commands_platforms.discord_skill_commands_by_category",
"hermes_cli.commands.discord_skill_commands_by_category",
return_value=(mock_categories, mock_uncategorized, 0),
):
adapter._register_slash_commands()
@@ -580,7 +580,7 @@ def test_register_skill_command_payload_fits_discord_8kb_limit(adapter):
]
with patch(
"hermes_cli.commands_platforms.discord_skill_commands_by_category",
"hermes_cli.commands.discord_skill_commands_by_category",
return_value=(large_categories, [], 0),
):
adapter._register_slash_commands()
@@ -69,7 +69,7 @@ class TestRefreshSkillGroup:
)
monkeypatch.setattr(
"hermes_cli.commands_platforms.discord_skill_commands_by_category",
"hermes_cli.commands.discord_skill_commands_by_category",
fake_collector,
)
@@ -113,7 +113,7 @@ class TestRegisterSkillGroupUsesInstanceState:
0,
)
monkeypatch.setattr(
"hermes_cli.commands_platforms.discord_skill_commands_by_category",
"hermes_cli.commands.discord_skill_commands_by_category",
fake_collector,
)
+1 -1
View File
@@ -95,7 +95,7 @@ class TestFormatSessionInfo:
},
)
with p1, p2, p3, patch(
"hermes_cli.config_providers.get_compatible_custom_providers",
"hermes_cli.config.get_compatible_custom_providers",
return_value=[
{
"name": "custom-local-agentw",
+1 -1
View File
@@ -501,7 +501,7 @@ async def test_context_command_keeps_configured_window_without_resident_agent():
with patch("gateway.run._load_gateway_config", return_value=config), patch(
"gateway.run._resolve_runtime_agent_kwargs", return_value=runtime
), patch(
"hermes_cli.config_providers.get_compatible_custom_providers",
"hermes_cli.config.get_compatible_custom_providers",
return_value=config["custom_providers"],
), patch(
"agent.model_metadata.get_model_context_length",
@@ -35,7 +35,7 @@ async def test_ensure_forum_commands_registers_once():
adapter = _make_test_adapter()
msg = _forum_message(chat_id=-123, is_forum=True)
with patch("hermes_cli.commands_platforms.telegram_menu_commands") as mock_menu:
with patch("hermes_cli.commands.telegram_menu_commands") as mock_menu:
mock_menu.return_value = ([("new", "Start new session"), ("help", "Show help")], 0)
with patch("telegram.BotCommand") as MockBotCommand:
instances = []
@@ -73,7 +73,7 @@ async def test_ensure_forum_commands_race_safety():
adapter = _make_test_adapter()
msg = _forum_message(chat_id=-789, is_forum=True)
with patch("hermes_cli.commands_platforms.telegram_menu_commands") as mock_menu:
with patch("hermes_cli.commands.telegram_menu_commands") as mock_menu:
mock_menu.return_value = ([("new", "Start new session")], 0)
with patch("telegram.BotCommand"):
with patch("telegram.BotCommandScopeChat"):
@@ -27,8 +27,6 @@ single branch cannot silently revert #32243.
from __future__ import annotations
from hermes_cli import runtime_provider as rp
from hermes_cli import runtime_provider_custom
from agent import credential_pool
class TestExplicitRuntimeForAnthropic:
@@ -148,10 +146,10 @@ class TestCustomProviderUrlFallback:
def select(self):
return _Entry()
monkeypatch.setattr(credential_pool, "custom_provider_pool_key_candidates", lambda *a, **k: ["custom:my-claude"])
monkeypatch.setattr(rp, "custom_provider_pool_key_candidates", lambda *a, **k: ["custom:my-claude"])
monkeypatch.setattr(rp, "load_pool", lambda key: _Pool())
resolved = runtime_provider_custom._try_resolve_from_custom_pool(
resolved = rp._try_resolve_from_custom_pool(
"https://api.anthropic.com",
"custom",
)
+3 -4
View File
@@ -6,7 +6,7 @@ canonical set consumed by ``agent_init`` is now {chat_completions,
codex_responses, anthropic_messages, bedrock_converse, codex_app_server},
and an unrecognized value was silently ignored at BOTH consumption sites:
* ``hermes_cli.config_providers._normalize_custom_provider_entry`` passed the raw
* ``hermes_cli.config._normalize_custom_provider_entry`` passed the raw
string through, so ``agent_init``'s accepted-set check dropped it and
fell through to hostname detection.
* ``hermes_cli.runtime_provider._parse_api_mode`` returned None, with the
@@ -28,9 +28,8 @@ from __future__ import annotations
import pytest
from hermes_cli.config_providers import _canonical_api_mode, _normalize_custom_provider_entry
from hermes_cli.config import _canonical_api_mode, _normalize_custom_provider_entry
from hermes_cli.runtime_provider import _parse_api_mode, _VALID_API_MODES
import hermes_cli.config_providers
class TestCanonicalApiMode:
@@ -66,7 +65,7 @@ class TestCanonicalApiMode:
def test_every_alias_lands_in_the_valid_set(self):
"""Contract: aliasing must never produce a value the runtime rejects."""
from hermes_cli.config_providers import _API_MODE_ALIASES
from hermes_cli.config import _API_MODE_ALIASES
for target in _API_MODE_ALIASES.values():
assert target in _VALID_API_MODES
+8 -3
View File
@@ -6,9 +6,14 @@ from unittest.mock import MagicMock, patch
import yaml
from cli import HermesCLI
from hermes_cli.commands import GATEWAY_KNOWN_COMMANDS, SUBCOMMANDS, gateway_help_lines, resolve_command
from hermes_cli.commands_completion import SlashCommandCompleter
from hermes_cli.commands_platforms import telegram_bot_commands
from hermes_cli.commands import (
GATEWAY_KNOWN_COMMANDS,
SUBCOMMANDS,
SlashCommandCompleter,
gateway_help_lines,
resolve_command,
telegram_bot_commands,
)
from prompt_toolkit.completion import CompleteEvent
from prompt_toolkit.document import Document
@@ -14,7 +14,7 @@ from __future__ import annotations
from pathlib import Path
from typing import Iterable
from hermes_cli.commands_completion import SlashCommandCompleter
from hermes_cli.commands import SlashCommandCompleter
def _run(tmp_path: Path, word: str) -> list[tuple[str, str]]:
+3 -6
View File
@@ -22,7 +22,6 @@ def _bitwarden_config(*, enabled: bool = True, server_url: str = "") -> dict:
def test_status_surfaces_failed_token_validation(monkeypatch, capsys):
from hermes_cli import secrets_cli
from agent.secret_sources import bitwarden as bw
seen = {}
@@ -33,7 +32,7 @@ def test_status_surfaces_failed_token_validation(monkeypatch, capsys):
)
monkeypatch.setenv("BWS_ACCESS_TOKEN", "0.invalid-token")
monkeypatch.setattr(
bw,
secrets_cli.bw,
"find_bws",
lambda install_if_missing=False: Path("/tmp/bws"),
)
@@ -69,12 +68,11 @@ def test_status_surfaces_failed_token_validation(monkeypatch, capsys):
def test_status_warns_when_token_does_not_look_like_bsm_token(monkeypatch, capsys):
from hermes_cli import secrets_cli
from agent.secret_sources import bitwarden as bw
monkeypatch.setattr(secrets_cli, "load_config", lambda: _bitwarden_config())
monkeypatch.setenv("BWS_ACCESS_TOKEN", "not-a-bitwarden-token")
monkeypatch.setattr(
bw,
secrets_cli.bw,
"find_bws",
lambda install_if_missing=False: Path("/tmp/bws"),
)
@@ -95,12 +93,11 @@ def test_status_warns_when_token_does_not_look_like_bsm_token(monkeypatch, capsy
def test_status_marks_validation_as_not_checked_without_bws_binary(monkeypatch, capsys):
from hermes_cli import secrets_cli
from agent.secret_sources import bitwarden as bw
monkeypatch.setattr(secrets_cli, "load_config", lambda: _bitwarden_config())
monkeypatch.setenv("BWS_ACCESS_TOKEN", "0.token-present")
monkeypatch.setattr(
bw,
secrets_cli.bw,
"find_bws",
lambda install_if_missing=False: None,
)
@@ -18,7 +18,6 @@ from __future__ import annotations
import pytest
from hermes_cli import runtime_provider as rp
from hermes_cli import runtime_provider_custom
PROVIDER_KEY = "my-endpoint"
DISPLAY_NAME = "My Endpoint Display"
@@ -50,18 +49,18 @@ def keyed_provider_config(monkeypatch):
def test_display_name_heals_to_the_config_key_identity(keyed_provider_config):
"""The regression: the display-name spelling must not mint a second identity."""
assert runtime_provider_custom.canonical_custom_identity(config_provider=DISPLAY_NAME) == CANONICAL
assert rp.canonical_custom_identity(config_provider=DISPLAY_NAME) == CANONICAL
def test_config_model_provider_display_name_heals_too(keyed_provider_config, monkeypatch):
"""Same path reached through ``config.model.provider`` rather than an argument."""
monkeypatch.setattr(rp, "_get_model_config", lambda: {"provider": DISPLAY_NAME})
assert runtime_provider_custom.canonical_custom_identity() == CANONICAL
assert rp.canonical_custom_identity() == CANONICAL
def test_config_key_spelling_still_resolves(keyed_provider_config):
"""The spelling that already worked keeps working."""
assert runtime_provider_custom.canonical_custom_identity(config_provider=PROVIDER_KEY) == CANONICAL
assert rp.canonical_custom_identity(config_provider=PROVIDER_KEY) == CANONICAL
def test_all_recovery_sources_agree_on_one_identity(keyed_provider_config):
@@ -70,16 +69,16 @@ def test_all_recovery_sources_agree_on_one_identity(keyed_provider_config):
Three sources feeding the same session-identity slot is only safe while
they agree; a divergent one silently splits an endpoint in two.
"""
by_url = runtime_provider_custom.canonical_custom_identity(base_url=BASE_URL)
by_model = runtime_provider_custom.canonical_custom_identity(model=MODEL)
by_config = runtime_provider_custom.canonical_custom_identity(config_provider=DISPLAY_NAME)
by_url = rp.canonical_custom_identity(base_url=BASE_URL)
by_model = rp.canonical_custom_identity(model=MODEL)
by_config = rp.canonical_custom_identity(config_provider=DISPLAY_NAME)
assert {by_url, by_model, by_config} == {CANONICAL}
def test_unconfigured_candidate_still_returns_none(keyed_provider_config):
"""Fail-closed contract: never invent an identity resolution can't honour."""
assert runtime_provider_custom.canonical_custom_identity(config_provider="not-a-configured-entry") is None
assert rp.canonical_custom_identity(config_provider="not-a-configured-entry") is None
def test_legacy_unkeyed_entry_keeps_its_name_identity(monkeypatch):
@@ -98,7 +97,7 @@ def test_legacy_unkeyed_entry_keeps_its_name_identity(monkeypatch):
monkeypatch.setattr("hermes_cli.config.load_config", lambda *a, **k: config)
monkeypatch.setattr(rp, "_get_model_config", lambda: {})
assert runtime_provider_custom.canonical_custom_identity(config_provider="Legacy Endpoint") == "custom:legacy-endpoint"
assert rp.canonical_custom_identity(config_provider="Legacy Endpoint") == "custom:legacy-endpoint"
class TestIsRoutableProvider:
@@ -109,25 +108,25 @@ class TestIsRoutableProvider:
"""
def test_empty_auto_and_builtin_are_routable(self, keyed_provider_config):
assert runtime_provider_custom.is_routable_provider(None) is True
assert runtime_provider_custom.is_routable_provider("") is True
assert runtime_provider_custom.is_routable_provider("auto") is True
assert runtime_provider_custom.is_routable_provider("openrouter") is True
assert rp.is_routable_provider(None) is True
assert rp.is_routable_provider("") is True
assert rp.is_routable_provider("auto") is True
assert rp.is_routable_provider("openrouter") is True
def test_bare_custom_is_not_routable(self, keyed_provider_config):
# The resolved billing class, not a routable identity — restore
# paths must heal it (canonical_custom_identity) or fall back.
assert runtime_provider_custom.is_routable_provider("custom") is False
assert rp.is_routable_provider("custom") is False
def test_registered_names_are_routable(self, keyed_provider_config):
assert runtime_provider_custom.is_routable_provider(PROVIDER_KEY) is True
assert runtime_provider_custom.is_routable_provider(CANONICAL) is True
assert rp.is_routable_provider(PROVIDER_KEY) is True
assert rp.is_routable_provider(CANONICAL) is True
def test_stale_name_is_not_routable(self, keyed_provider_config):
# Same endpoint family, but the OLD slug no longer matches any
# configured entry — the regression this gate exists for.
assert runtime_provider_custom.is_routable_provider("stale-endpoint") is False
assert runtime_provider_custom.is_routable_provider("custom:stale-endpoint") is False
assert rp.is_routable_provider("stale-endpoint") is False
assert rp.is_routable_provider("custom:stale-endpoint") is False
def test_legacy_unkeyed_name_is_routable(self, monkeypatch):
config = {
@@ -144,6 +143,6 @@ class TestIsRoutableProvider:
monkeypatch.setattr("hermes_cli.config.load_config", lambda *a, **k: config)
monkeypatch.setattr(rp, "_get_model_config", lambda: {})
assert runtime_provider_custom.is_routable_provider("legacy-endpoint") is True
assert runtime_provider_custom.is_routable_provider("custom:legacy-endpoint") is True
assert runtime_provider_custom.is_routable_provider("Legacy Endpoint") is True
assert rp.is_routable_provider("legacy-endpoint") is True
assert rp.is_routable_provider("custom:legacy-endpoint") is True
assert rp.is_routable_provider("Legacy Endpoint") is True
+33 -18
View File
@@ -4,13 +4,28 @@ from prompt_toolkit.completion import CompleteEvent
from prompt_toolkit.document import Document
from hermes_cli.commands import (
COMMAND_REGISTRY, COMMANDS, COMMANDS_BY_CATEGORY, CommandDef, GATEWAY_KNOWN_COMMANDS, SUBCOMMANDS,
command_desktop_meta, gateway_help_lines, infer_argument_mode, resolve_command
)
from hermes_cli.commands_completion import SlashCommandAutoSuggest, SlashCommandCompleter
from hermes_cli.commands_platforms import (
_CMD_NAME_LIMIT, _SLACK_RESERVED_COMMANDS, _SLACK_VIA_HERMES_ONLY, _clamp_command_names, _sanitize_telegram_name,
slack_app_manifest, slack_native_slashes, slack_subcommand_map, telegram_bot_commands, telegram_menu_commands
COMMAND_REGISTRY,
COMMANDS,
COMMANDS_BY_CATEGORY,
CommandDef,
GATEWAY_KNOWN_COMMANDS,
SUBCOMMANDS,
SlashCommandAutoSuggest,
SlashCommandCompleter,
_CMD_NAME_LIMIT,
_SLACK_RESERVED_COMMANDS,
_SLACK_VIA_HERMES_ONLY,
_clamp_command_names,
_sanitize_telegram_name,
command_desktop_meta,
gateway_help_lines,
infer_argument_mode,
resolve_command,
slack_app_manifest,
slack_native_slashes,
slack_subcommand_map,
telegram_bot_commands,
telegram_menu_commands,
)
@@ -574,7 +589,7 @@ class TestGatewaySkillCollector:
def test_long_skill_name_clamped_but_cmd_key_retained(self, tmp_path):
from unittest.mock import patch
from hermes_cli.commands_platforms import _collect_gateway_skill_entries
from hermes_cli.commands import _collect_gateway_skill_entries
long_name = "this-is-a-very-long-skill-name-that-exceeds-limit"
skills_dir = tmp_path / "skills"
@@ -602,7 +617,7 @@ class TestGatewaySkillCollector:
def test_cap_trims_skills_only(self, tmp_path):
from unittest.mock import patch
from hermes_cli.commands_platforms import _collect_gateway_skill_entries
from hermes_cli.commands import _collect_gateway_skill_entries
skills_dir = tmp_path / "skills"
skills_dir.mkdir()
@@ -799,11 +814,11 @@ class TestTelegramMenuCommands:
menu_cfg = {"max_commands": 2, "priority_mode": "prepend", "priority": ["gym"]}
with (
patch("hermes_cli.commands_platforms.telegram_bot_commands", return_value=fake_core),
patch("hermes_cli.commands.telegram_bot_commands", return_value=fake_core),
patch("agent.skill_commands.get_skill_commands", return_value=fake_cmds),
patch("hermes_cli.plugins.get_plugin_commands", return_value=fake_plugins),
patch("tools.skills_tool.SKILLS_DIR", local_dir),
patch("hermes_cli.commands_platforms._telegram_command_menu_config", return_value=menu_cfg),
patch("hermes_cli.commands._telegram_command_menu_config", return_value=menu_cfg),
):
menu, hidden = telegram_menu_commands(max_commands=len(fake_core))
@@ -840,10 +855,10 @@ class TestTelegramMenuCommands:
menu_cfg = {"max_commands": 2, "priority_mode": "prepend", "priority": []}
with (
patch("hermes_cli.commands_platforms.telegram_bot_commands", return_value=fake_core),
patch("hermes_cli.commands.telegram_bot_commands", return_value=fake_core),
patch("agent.skill_commands.get_skill_commands", return_value=fake_cmds),
patch("tools.skills_tool.SKILLS_DIR", local_dir),
patch("hermes_cli.commands_platforms._telegram_command_menu_config", return_value=menu_cfg),
patch("hermes_cli.commands._telegram_command_menu_config", return_value=menu_cfg),
):
menu, hidden = telegram_menu_commands(max_commands=2)
@@ -876,10 +891,10 @@ class TestTelegramMenuCommands:
}
with (
patch("hermes_cli.commands_platforms.telegram_bot_commands", return_value=fake_core),
patch("hermes_cli.commands.telegram_bot_commands", return_value=fake_core),
patch("agent.skill_commands.get_skill_commands", return_value=fake_cmds),
patch("tools.skills_tool.SKILLS_DIR", local_dir),
patch("hermes_cli.commands_platforms._telegram_command_menu_config", return_value=menu_cfg),
patch("hermes_cli.commands._telegram_command_menu_config", return_value=menu_cfg),
):
menu, hidden = telegram_menu_commands(max_commands=1)
@@ -907,7 +922,7 @@ class TestTelegramMenuCommands:
patch("hermes_cli.plugins.get_plugin_commands", return_value=fake_plugins),
patch("agent.skill_commands.get_skill_commands", return_value={}),
patch("tools.skills_tool.SKILLS_DIR", local_dir),
patch("hermes_cli.commands_platforms._telegram_command_menu_config", return_value=menu_cfg),
patch("hermes_cli.commands._telegram_command_menu_config", return_value=menu_cfg),
):
menu, hidden = telegram_menu_commands(max_commands=1)
@@ -942,7 +957,7 @@ class TestTelegramMenuCommands:
def test_scalar_configured_priority_is_accepted_as_one_command(self):
"""The config CLI's scalar value form must work for a single priority."""
from unittest.mock import patch
from hermes_cli.commands_platforms import _telegram_command_menu_config
from hermes_cli.commands import _telegram_command_menu_config
raw_config = {
"platforms": {
@@ -965,7 +980,7 @@ class TestTelegramMenuCommands:
# Discord skill commands grouped by category
# ---------------------------------------------------------------------------
from hermes_cli.commands_platforms import discord_skill_commands_by_category
from hermes_cli.commands import discord_skill_commands_by_category # noqa: E402
class TestDiscordSkillCommandsByCategory:
+22 -5
View File
@@ -9,12 +9,29 @@ import pytest
import yaml
from hermes_cli.config import (
DEFAULT_CONFIG, InvalidUserConfigError, check_config_version, get_hermes_home, ensure_hermes_home,
_explicit_config_paths, _normalize_max_turns_config, load_config, load_env, migrate_config, read_raw_config,
remove_env_value, save_config, save_env_value, save_env_value_secure, sanitize_env_file, set_config_value,
unset_config_value, write_platform_config_field, _sanitize_env_lines
DEFAULT_CONFIG,
InvalidUserConfigError,
check_config_version,
get_hermes_home,
ensure_hermes_home,
get_compatible_custom_providers,
_explicit_config_paths,
_normalize_max_turns_config,
is_provider_enabled,
load_config,
load_env,
migrate_config,
read_raw_config,
remove_env_value,
save_config,
save_env_value,
save_env_value_secure,
sanitize_env_file,
set_config_value,
unset_config_value,
write_platform_config_field,
_sanitize_env_lines,
)
from hermes_cli.config_providers import get_compatible_custom_providers, is_provider_enabled
class TestGetHermesHome:
@@ -8,7 +8,10 @@ from __future__ import annotations
from unittest.mock import patch
from hermes_cli.config_providers import get_custom_provider_context_length, get_custom_provider_model_capability
from hermes_cli.config import (
get_custom_provider_context_length,
get_custom_provider_model_capability,
)
class TestGetCustomProviderContextLength:
@@ -6,9 +6,11 @@ PR #3526 salvage — user-configurable extra HTTP headers on LLM API calls
import json
from hermes_cli.config_providers import (
_normalize_custom_provider_entry, apply_custom_provider_extra_headers_to_client_kwargs,
get_custom_provider_extra_headers, normalize_extra_headers
from hermes_cli.config import (
_normalize_custom_provider_entry,
apply_custom_provider_extra_headers_to_client_kwargs,
get_custom_provider_extra_headers,
normalize_extra_headers,
)
from hermes_cli import models as models_mod
@@ -9,7 +9,6 @@ end-to-end persist/resume round-trip.
"""
import hermes_cli.runtime_provider as rp
from hermes_cli import runtime_provider_custom
def test_matches_legacy_custom_providers_list(monkeypatch):
@@ -23,7 +22,7 @@ def test_matches_legacy_custom_providers_list(monkeypatch):
},
)
assert (
runtime_provider_custom.find_custom_provider_identity("https://api.mimo.example/v1")
rp.find_custom_provider_identity("https://api.mimo.example/v1")
== "custom:mimo-v2.5-pro"
)
@@ -35,7 +34,7 @@ def test_matches_providers_dict_by_key(monkeypatch):
lambda: {"providers": {"local": {"api": "http://127.0.0.1:8000/v1"}}},
)
assert (
runtime_provider_custom.find_custom_provider_identity("http://127.0.0.1:8000/v1")
rp.find_custom_provider_identity("http://127.0.0.1:8000/v1")
== "custom:local"
)
@@ -54,10 +53,10 @@ def test_matches_providers_dict_by_stable_key_not_display_name(monkeypatch):
"load_config",
lambda: config,
)
slug = runtime_provider_custom.find_custom_provider_identity("http://127.0.0.1:8000/v1")
slug = rp.find_custom_provider_identity("http://127.0.0.1:8000/v1")
assert slug == "custom:local-127.0.0.1:8000"
entry = runtime_provider_custom._get_named_custom_provider(slug)
entry = rp._get_named_custom_provider(slug)
assert entry is not None
assert entry["name"] == "Local Ollama"
@@ -73,7 +72,7 @@ def test_match_ignores_trailing_slash_and_case(monkeypatch):
},
)
assert (
runtime_provider_custom.find_custom_provider_identity("http://localhost:8000/v1")
rp.find_custom_provider_identity("http://localhost:8000/v1")
== "custom:local"
)
@@ -88,15 +87,15 @@ def test_no_match_returns_none(monkeypatch):
]
},
)
assert runtime_provider_custom.find_custom_provider_identity("https://api.mimo.example/v1") is None
assert rp.find_custom_provider_identity("https://api.mimo.example/v1") is None
def test_empty_base_url_returns_none(monkeypatch):
monkeypatch.setattr(
rp, "load_config", lambda: {"custom_providers": [{"name": "x"}]}
)
assert runtime_provider_custom.find_custom_provider_identity("") is None
assert runtime_provider_custom.find_custom_provider_identity(None) is None
assert rp.find_custom_provider_identity("") is None
assert rp.find_custom_provider_identity(None) is None
def test_identity_resolves_back_through_named_lookup(monkeypatch):
@@ -113,10 +112,10 @@ def test_identity_resolves_back_through_named_lookup(monkeypatch):
}
monkeypatch.setattr(rp, "load_config", lambda: config)
slug = runtime_provider_custom.find_custom_provider_identity("https://api.mimo.example/v1")
slug = rp.find_custom_provider_identity("https://api.mimo.example/v1")
assert slug == "custom:mimo-v2.5-pro"
entry = runtime_provider_custom._get_named_custom_provider(slug)
entry = rp._get_named_custom_provider(slug)
assert entry is not None
assert entry["base_url"] == "https://api.mimo.example/v1"
assert entry["api_key"] == "sk-entry"
@@ -8,8 +8,10 @@ caller's entry, corrupting the shared cache for every subsequent reader.
import copy
from hermes_cli.config_providers import (
_normalize_custom_provider_entry, get_compatible_custom_providers, providers_dict_to_custom_providers
from hermes_cli.config import (
_normalize_custom_provider_entry,
get_compatible_custom_providers,
providers_dict_to_custom_providers,
)
+4 -1
View File
@@ -1,6 +1,9 @@
"""Tests for per-provider TLS settings in custom_providers config."""
from hermes_cli.config_providers import apply_custom_provider_tls_to_client_kwargs, get_custom_provider_tls_settings
from hermes_cli.config import (
apply_custom_provider_tls_to_client_kwargs,
get_custom_provider_tls_settings,
)
def test_get_custom_provider_tls_settings_matches_base_url():
@@ -23,7 +23,7 @@ def test_clamp_collision_emits_warning_naming_both_skills(
tmp_path: Path, caplog
) -> None:
"""Two skills with identical first 32 chars — warning names both."""
from hermes_cli.commands_platforms import discord_skill_commands_by_category
from hermes_cli.commands import discord_skill_commands_by_category
# Craft cmd_keys that share the first 32 chars.
# 40-char prefix 'skill-collision-prefix-identical-first-32'
@@ -98,7 +98,7 @@ def test_long_skill_name_preserves_cmd_key_through_by_category(
This is the actual runtime path used by the Discord adapter via
``_refresh_skill_catalog_state``.
"""
from hermes_cli.commands_platforms import discord_skill_commands_by_category
from hermes_cli.commands import discord_skill_commands_by_category
skills_dir = tmp_path / "skills"
skills_dir.mkdir()
@@ -66,7 +66,7 @@ def test_get_named_custom_provider_exposes_provider_key_and_key_env(
)
monkeypatch.setenv("B_AI_API_KEY", "sk-from-env-not-the-pool")
from hermes_cli.runtime_provider_custom import _get_named_custom_provider
from hermes_cli.runtime_provider import _get_named_custom_provider
entry = _get_named_custom_provider("b-ai")
assert entry is not None
+1 -1
View File
@@ -78,7 +78,7 @@ class TestHostMandatedMetaResponses:
class TestMetaConfigRoundtrip:
def test_providers_meta_api_mode_roundtrip(self):
from hermes_cli.config_providers import _normalize_custom_provider_entry
from hermes_cli.config import _normalize_custom_provider_entry
entry = {"name": "Meta", "base_url": "https://api.meta.ai/v1", "api_mode": "codex_responses"}
normalized = _normalize_custom_provider_entry(entry)
@@ -18,7 +18,6 @@ import yaml
from hermes_cli.model_switch import list_authenticated_providers, switch_model
from hermes_cli.model_switch_providers import _fetch_picker_live_models, _save_discovered_models_to_config
from hermes_cli.providers import resolve_provider_full
from hermes_cli import config_providers
_MOCK_VALIDATION = {
@@ -2190,7 +2189,7 @@ def test_legacy_sentinel_catalog_still_resolves_and_migrates(tmp_path, monkeypat
# (b) sentinels never surface as model IDs.
assert _declared_model_ids(legacy_entry["models"]) == _LOCAL_CATALOG
normalized = config_providers._normalize_custom_provider_entry(dict(legacy_entry))
normalized = config_mod._normalize_custom_provider_entry(dict(legacy_entry))
assert normalized is not None
assert normalized["models_discovered"] is True
assert list(normalized["models"]) == _LOCAL_CATALOG
+1 -1
View File
@@ -7,7 +7,7 @@ import pytest
from prompt_toolkit.document import Document
from prompt_toolkit.formatted_text import to_plain_text
from hermes_cli.commands_completion import SlashCommandCompleter, _file_size_label
from hermes_cli.commands import SlashCommandCompleter, _file_size_label
def _display_names(completions):
+2 -2
View File
@@ -83,7 +83,7 @@ class TestDeclarationParsing:
def test_manifest_field_lands_on_parsed_manifest(self, tmp_path):
"""PluginManifest picks up ``capabilities:`` from plugin.yaml."""
from hermes_cli.plugins_manifest import parse_manifest_file
from hermes_cli.plugins import parse_manifest_file
plugin_dir = tmp_path / "capplug"
plugin_dir.mkdir()
@@ -99,7 +99,7 @@ class TestDeclarationParsing:
assert manifest.capabilities == ["tools.override"]
def test_manifest_without_capabilities_field(self, tmp_path):
from hermes_cli.plugins_manifest import parse_manifest_file
from hermes_cli.plugins import parse_manifest_file
plugin_dir = tmp_path / "plainplug"
plugin_dir.mkdir()
+8 -4
View File
@@ -21,8 +21,12 @@ import threading
import pytest
from hermes_cli.plugins import PluginContext, PluginManager, PluginManifest
from hermes_cli.plugins_dispatch import _EVENT_EMIT_DEPTH_CAP
from hermes_cli.plugins import (
_EVENT_EMIT_DEPTH_CAP,
PluginContext,
PluginManager,
PluginManifest,
)
# ── Fixtures ─────────────────────────────────────────────────────────────────
@@ -407,7 +411,7 @@ def test_manifest_parse_reads_emits_listens(tmp_path):
encoding="utf-8",
)
from hermes_cli.plugins_manifest import parse_manifest_file
from hermes_cli.plugins import parse_manifest_file
manifest = parse_manifest_file(manifest_file, plugin_dir, "user", "")
assert manifest is not None
@@ -425,7 +429,7 @@ def test_manifest_parse_absent_emits_listens(tmp_path):
yaml.safe_dump({"name": "bare"}), encoding="utf-8"
)
from hermes_cli.plugins_manifest import parse_manifest_file
from hermes_cli.plugins import parse_manifest_file
manifest = parse_manifest_file(manifest_file, plugin_dir, "user", "")
assert manifest is not None
+7 -2
View File
@@ -11,8 +11,13 @@ import logging
import pytest
import yaml
from hermes_cli.plugins import PluginManager, PluginManifest, resolve_plugin_load_order
from hermes_cli.plugins_manifest import SUPPORTED_MANIFEST_VERSION, validate_config_schema
from hermes_cli.plugins import (
PluginManager,
PluginManifest,
SUPPORTED_MANIFEST_VERSION,
resolve_plugin_load_order,
validate_config_schema,
)
def _write_plugin(base, name, manifest_extra=None, register_body="pass"):
@@ -5,8 +5,12 @@ from types import MappingProxyType
import pytest
from hermes_cli.plugins import PluginContext, PluginManager, PluginManifest
from hermes_cli.plugins_dispatch import MAX_SYSTEM_PROMPT_SECTIONS_TOTAL_CHARS
from hermes_cli.plugins import (
MAX_SYSTEM_PROMPT_SECTIONS_TOTAL_CHARS,
PluginContext,
PluginManager,
PluginManifest,
)
def _context(manager: PluginManager, name: str = "example-plugin") -> PluginContext:
+32 -25
View File
@@ -12,12 +12,20 @@ import pytest
import yaml
from hermes_cli.plugins import (
VALID_HOOKS, PluginContext, PluginManager, PluginManifest, _dispatch_pre_tool_call_hooks,
get_plugin_command_handler, get_plugin_commands, _get_pre_tool_call_directive_details,
get_pre_verify_continue_message, has_middleware, resolve_plugin_command_result
ENTRY_POINTS_GROUP,
VALID_HOOKS,
PluginContext,
PluginManager,
PluginManifest,
_dispatch_pre_tool_call_hooks,
get_plugin_command_handler,
get_plugin_commands,
get_pre_tool_call_block_message,
get_pre_verify_continue_message,
has_middleware,
resolve_plugin_command_result,
_portable_skill_namespace,
)
from hermes_cli.plugins_discovery import ENTRY_POINTS_GROUP
from hermes_cli.plugins_manifest import _portable_skill_namespace
from hermes_cli.relay_plugin_cutover import RELAY_PLUGINS_CONFIG_ENV
from hermes_cli.middleware import (
VALID_MIDDLEWARE,
@@ -1140,8 +1148,10 @@ class TestForceReloadSymmetry:
"""Timed-out pre_tool_call must return a block directive, not allow."""
import time
from hermes_cli.plugins import resolve_pre_tool_block
from hermes_cli.plugins_dispatch import _PRE_TOOL_CALL_TIMEOUT_BLOCK_MESSAGE
from hermes_cli.plugins import (
_PRE_TOOL_CALL_TIMEOUT_BLOCK_MESSAGE,
resolve_pre_tool_block,
)
monkeypatch.setattr(
"hermes_cli.plugins._resolve_hook_callback_timeout", lambda: 0.1
@@ -1176,7 +1186,7 @@ class TestForceReloadSymmetry:
"""E2E: timed-out pre_tool_call blocks handle_function_call before dispatch."""
import json
from hermes_cli.plugins_dispatch import _PRE_TOOL_CALL_TIMEOUT_BLOCK_MESSAGE
from hermes_cli.plugins import _PRE_TOOL_CALL_TIMEOUT_BLOCK_MESSAGE
monkeypatch.setattr(
"hermes_cli.plugins._resolve_hook_callback_timeout", lambda: 0.1
@@ -1271,8 +1281,7 @@ class TestPreToolCallBlocking:
"hermes_cli.plugins.invoke_hook",
lambda hook_name, **kwargs: [{"action": "block", "message": "blocked by plugin"}],
)
details = _get_pre_tool_call_directive_details("todo", {}, task_id="t1")
assert (details.action, details.message) == ("block", "blocked by plugin")
assert get_pre_tool_call_block_message("todo", {}, task_id="t1") == "blocked by plugin"
class TestPreToolCallDirective:
@@ -1280,7 +1289,7 @@ class TestPreToolCallDirective:
def test_first_party_observer_receives_pre_tool_call(self, monkeypatch):
from hermes_cli import observability
from hermes_cli.plugins import _get_pre_tool_call_directive_details
from hermes_cli.plugins import get_pre_tool_call_directive
observed = []
monkeypatch.setattr(
@@ -1293,14 +1302,13 @@ class TestPreToolCallDirective:
lambda hook_name, **kwargs: [],
)
details = _get_pre_tool_call_directive_details(
assert get_pre_tool_call_directive(
"write_file",
{"path": "README.md"},
task_id="task-1",
session_id="session-1",
tool_call_id="call-1",
)
assert (details.action, details.message) == (None, None)
) == (None, None)
assert observed == [
(
"pre_tool_call",
@@ -1318,25 +1326,24 @@ class TestPreToolCallDirective:
]
def test_approve_directive_returned(self, monkeypatch):
from hermes_cli.plugins import _get_pre_tool_call_directive_details
from hermes_cli.plugins import get_pre_tool_call_directive
monkeypatch.setattr(
"hermes_cli.plugins.invoke_hook",
lambda hook_name, **kwargs: [
{"action": "approve", "message": "needs human ok"}
],
)
details = _get_pre_tool_call_directive_details("write_file", {})
assert (details.action, details.message) == ("approve", "needs human ok")
assert get_pre_tool_call_directive("write_file", {}) == (
"approve", "needs human ok")
def test_approve_without_message_is_valid(self, monkeypatch):
"""approve may omit a message (block may not)."""
from hermes_cli.plugins import _get_pre_tool_call_directive_details
from hermes_cli.plugins import get_pre_tool_call_directive
monkeypatch.setattr(
"hermes_cli.plugins.invoke_hook",
lambda hook_name, **kwargs: [{"action": "approve"}],
)
details = _get_pre_tool_call_directive_details("write_file", {})
assert (details.action, details.message) == ("approve", None)
assert get_pre_tool_call_directive("write_file", {}) == ("approve", None)
class TestResolvePreToolBlock:
@@ -1570,7 +1577,7 @@ class TestThreadToolWhitelist:
)
set_thread_tool_whitelist({"memory", "skill_manage"})
try:
assert _get_pre_tool_call_directive_details("memory", {}).action != "block"
assert get_pre_tool_call_block_message("memory", {}) is None
finally:
clear_thread_tool_whitelist()
@@ -1589,7 +1596,7 @@ class TestThreadToolWhitelist:
clear_thread_tool_whitelist()
# After clearing, any tool should pass through to plugin hooks (which
# return [] here, so result is None).
assert _get_pre_tool_call_directive_details("terminal", {}).action != "block"
assert get_pre_tool_call_block_message("terminal", {}) is None
def test_whitelist_is_thread_local(self, monkeypatch):
"""Setting a whitelist in one thread must NOT leak into another."""
@@ -1608,18 +1615,18 @@ class TestThreadToolWhitelist:
# Main thread: install a restrictive whitelist.
set_thread_tool_whitelist({"memory"})
try:
assert _get_pre_tool_call_directive_details("terminal", {}).action == "block"
assert get_pre_tool_call_block_message("terminal", {}) is not None
# Worker thread: should NOT inherit main thread's whitelist.
result = {}
def worker():
result["msg"] = _get_pre_tool_call_directive_details("terminal", {}).action
result["msg"] = get_pre_tool_call_block_message("terminal", {})
t = threading.Thread(target=worker)
t.start()
t.join()
assert result["msg"] != "block", (
assert result["msg"] is None, (
"thread-local whitelist leaked across threads"
)
finally:
@@ -8,7 +8,10 @@ import logging
import pytest
from hermes_cli.config_providers import _PROVIDER_NORMALIZE_WARNED, _normalize_custom_provider_entry
from hermes_cli.config import (
_PROVIDER_NORMALIZE_WARNED,
_normalize_custom_provider_entry,
)
class TestNormalizeCustomProviderEntry:
@@ -67,7 +70,7 @@ class TestNormalizeCustomProviderEntry:
def test_numeric_yaml_name_and_key_become_strings(self):
"""Unquoted YAML `name: 2070` / key 2070 must not be dropped as non-str."""
from hermes_cli.config_providers import find_provider_entry, stringify_provider_map
from hermes_cli.config import find_provider_entry, stringify_provider_map
result = _normalize_custom_provider_entry(
{"name": 2070, "base_url": "http://192.168.1.10:8082/v1"},
@@ -1,6 +1,9 @@
"""Unit tests for resolve_ephemeral_system_prompt."""
"""Unit tests for resolve_ephemeral_system_prompt_from_config."""
from hermes_cli.personality import render_personality_prompt, resolve_ephemeral_system_prompt
from hermes_cli.config import (
render_personality_prompt,
resolve_ephemeral_system_prompt_from_config,
)
def test_resolve_uses_named_personality_when_set():
@@ -11,7 +14,7 @@ def test_resolve_uses_named_personality_when_set():
"personalities": {"helpful": "You are helpful."},
},
}
assert resolve_ephemeral_system_prompt(cfg) == "You are helpful."
assert resolve_ephemeral_system_prompt_from_config(cfg) == "You are helpful."
def test_resolve_falls_back_to_manual_system_prompt():
@@ -22,7 +25,7 @@ def test_resolve_falls_back_to_manual_system_prompt():
"personalities": {"helpful": "You are helpful."},
},
}
assert resolve_ephemeral_system_prompt(cfg) == "manual forever"
assert resolve_ephemeral_system_prompt_from_config(cfg) == "manual forever"
def test_resolve_ignores_unknown_personality_name():
@@ -33,7 +36,7 @@ def test_resolve_ignores_unknown_personality_name():
"personalities": {"helpful": "You are helpful."},
},
}
assert resolve_ephemeral_system_prompt(cfg) == "manual forever"
assert resolve_ephemeral_system_prompt_from_config(cfg) == "manual forever"
def test_resolve_renders_dict_personality():
@@ -50,7 +53,7 @@ def test_resolve_renders_dict_personality():
},
},
}
resolved = resolve_ephemeral_system_prompt(cfg)
resolved = resolve_ephemeral_system_prompt_from_config(cfg)
assert "You are an expert programmer." in resolved
assert "Tone: technical" in resolved
assert "Style: concise" in resolved
@@ -52,12 +52,12 @@ def test_sibling_delegates_are_late_bound_too():
marker = [{"name": "marker", "base_url": "http://m/v1"}]
with patch(
"hermes_cli.config_providers.get_compatible_custom_providers", return_value=marker
"hermes_cli.config.get_compatible_custom_providers", return_value=marker
):
assert rp.get_compatible_custom_providers({}) is marker
assert rp.get_compatible_custom_providers({}) is not marker
hdrs = {"X-Marker": "1"}
with patch("hermes_cli.config_providers.normalize_extra_headers", return_value=hdrs):
with patch("hermes_cli.config.normalize_extra_headers", return_value=hdrs):
assert rp.normalize_extra_headers(None) is hdrs
assert rp.normalize_extra_headers(None) is not hdrs
@@ -6,8 +6,6 @@ from types import SimpleNamespace
import pytest
from hermes_cli import runtime_provider as rp
from hermes_cli import runtime_provider_custom
from agent import credential_pool
def test_configured_api_key_provider_without_key_fails_closed(monkeypatch):
@@ -117,19 +115,19 @@ class TestCustomProviderPoolLoopbackNoKeyExemption:
('123') for a local Ollama endpoint must resolve to the same
"no-key-required" placeholder every other local no-auth path uses,
not the raw unusable value."""
monkeypatch.setattr(credential_pool, "custom_provider_pool_key_candidates", lambda base_url, provider_name=None: ["custom:local-ollama"])
monkeypatch.setattr(rp, "custom_provider_pool_key_candidates", lambda base_url, provider_name=None: ["custom:local-ollama"])
monkeypatch.setattr(rp, "load_pool", lambda pool_key: self._pool_with("123"))
result = runtime_provider_custom._try_resolve_from_custom_pool("http://localhost:11434/v1", "custom", None)
result = rp._try_resolve_from_custom_pool("http://localhost:11434/v1", "custom", None)
assert result is not None
assert result["api_key"] == "no-key-required"
def test_single_char_placeholder_key_also_exempted(self, monkeypatch):
monkeypatch.setattr(credential_pool, "custom_provider_pool_key_candidates", lambda base_url, provider_name=None: ["custom:local"])
monkeypatch.setattr(rp, "custom_provider_pool_key_candidates", lambda base_url, provider_name=None: ["custom:local"])
monkeypatch.setattr(rp, "load_pool", lambda pool_key: self._pool_with("m"))
result = runtime_provider_custom._try_resolve_from_custom_pool("http://127.0.0.1:11434/v1", "custom", None)
result = rp._try_resolve_from_custom_pool("http://127.0.0.1:11434/v1", "custom", None)
assert result["api_key"] == "no-key-required"
@@ -138,10 +136,10 @@ class TestCustomProviderPoolLoopbackNoKeyExemption:
remote endpoint with a genuinely-too-short key must NOT get a
free pass. The short value passes through unchanged, so the
downstream has_usable_secret() gate still catches it."""
monkeypatch.setattr(credential_pool, "custom_provider_pool_key_candidates", lambda base_url, provider_name=None: ["custom:remote"])
monkeypatch.setattr(rp, "custom_provider_pool_key_candidates", lambda base_url, provider_name=None: ["custom:remote"])
monkeypatch.setattr(rp, "load_pool", lambda pool_key: self._pool_with("xy"))
result = runtime_provider_custom._try_resolve_from_custom_pool("https://api.remote-vendor.example/v1", "custom", None)
result = rp._try_resolve_from_custom_pool("https://api.remote-vendor.example/v1", "custom", None)
assert result["api_key"] == "xy"
@@ -149,10 +147,10 @@ class TestCustomProviderPoolLoopbackNoKeyExemption:
"""Sanity: a genuinely usable key for a loopback endpoint (a real
API key happens to be configured for a local proxy, say) must not
be silently overwritten."""
monkeypatch.setattr(credential_pool, "custom_provider_pool_key_candidates", lambda base_url, provider_name=None: ["custom:local"])
monkeypatch.setattr(rp, "custom_provider_pool_key_candidates", lambda base_url, provider_name=None: ["custom:local"])
monkeypatch.setattr(rp, "load_pool", lambda pool_key: self._pool_with("sk-genuinely-long-real-key-12345"))
result = runtime_provider_custom._try_resolve_from_custom_pool("http://localhost:11434/v1", "custom", None)
result = rp._try_resolve_from_custom_pool("http://localhost:11434/v1", "custom", None)
assert result["api_key"] == "sk-genuinely-long-real-key-12345"
@@ -689,7 +687,7 @@ def test_named_custom_provider_filters_capabilities_at_lookup_boundary(monkeypat
),
)
provider = runtime_provider_custom._get_named_custom_provider("local")
provider = rp._get_named_custom_provider("local")
assert provider["capabilities"] == {"openai_native_compaction": True}
@@ -844,7 +842,7 @@ def test_named_custom_provider_wins_over_builtin_alias(monkeypatch):
},
)
entry = runtime_provider_custom._get_named_custom_provider("kimi")
entry = rp._get_named_custom_provider("kimi")
assert entry is not None
assert entry["base_url"] == "https://my-custom-kimi.example.com/v1"
@@ -1002,7 +1000,7 @@ class TestOllamaUrlSubstringLeak:
"http://127.0.0.1:9000/ollama.com/v1"
))
monkeypatch.setattr(rp, "load_pool", lambda provider: None)
monkeypatch.setattr(runtime_provider_custom, "_try_resolve_from_custom_pool", lambda *a, **k: None)
monkeypatch.setattr(rp, "_try_resolve_from_custom_pool", lambda *a, **k: None)
resolved = rp.resolve_runtime_provider(requested="custom")
@@ -1023,7 +1021,7 @@ class TestOllamaUrlSubstringLeak:
"http://ollama.com.attacker.test:9000/v1"
))
monkeypatch.setattr(rp, "load_pool", lambda provider: None)
monkeypatch.setattr(runtime_provider_custom, "_try_resolve_from_custom_pool", lambda *a, **k: None)
monkeypatch.setattr(rp, "_try_resolve_from_custom_pool", lambda *a, **k: None)
resolved = rp.resolve_runtime_provider(requested="custom")
@@ -1041,7 +1039,7 @@ class TestOllamaUrlSubstringLeak:
"https://ollama.com/v1"
))
monkeypatch.setattr(rp, "load_pool", lambda provider: None)
monkeypatch.setattr(runtime_provider_custom, "_try_resolve_from_custom_pool", lambda *a, **k: None)
monkeypatch.setattr(rp, "_try_resolve_from_custom_pool", lambda *a, **k: None)
resolved = rp.resolve_runtime_provider(requested="custom")
@@ -1217,7 +1215,7 @@ class TestProviderEntryApiKeyEnvAlias:
use `api_key_env`) resolve correctly."""
def test_snake_case_api_key_env_normalizes_to_key_env(self):
from hermes_cli.config_providers import _normalize_custom_provider_entry
from hermes_cli.config import _normalize_custom_provider_entry
entry = {
"name": "vendor",
"base_url": "https://api.vendor.example.com/v1",
@@ -1235,8 +1233,10 @@ class TestProviderEntryApiKeyEnvAlias:
assert "key_env" in _VALID_CUSTOM_PROVIDER_FIELDS
def test_extra_body_is_supported_schema(self):
from hermes_cli.config import _VALID_CUSTOM_PROVIDER_FIELDS
from hermes_cli.config_providers import _normalize_custom_provider_entry
from hermes_cli.config import (
_VALID_CUSTOM_PROVIDER_FIELDS,
_normalize_custom_provider_entry,
)
entry = {
"name": "vendor",
"base_url": "https://api.vendor.example.com/v1",
@@ -1630,9 +1630,9 @@ def test_resolve_named_custom_runtime_pool_result_includes_extra_headers(monkeyp
"source": "pool:lmstudio-pool",
"credential_pool": "fake-pool",
}
monkeypatch.setattr(runtime_provider_custom, "_try_resolve_from_custom_pool", lambda *a, **k: pool_return_value)
monkeypatch.setattr(rp, "_try_resolve_from_custom_pool", lambda *a, **k: pool_return_value)
monkeypatch.setattr(
runtime_provider_custom,
rp,
"_get_named_custom_provider",
lambda p: {
"name": "lmstudio",
@@ -1720,7 +1720,7 @@ def test_custom_provider_explicit_target_model_wins(monkeypatch):
provider's configured default model (regression: auxiliary slots such as
background-review resolve a concrete model and got default_model instead)."""
monkeypatch.setattr(
runtime_provider_custom,
rp,
"_get_named_custom_provider",
lambda p: {
"name": "myproxy",
@@ -1741,7 +1741,7 @@ def test_custom_provider_explicit_target_model_wins(monkeypatch):
def test_custom_provider_without_target_model_keeps_default(monkeypatch):
"""No target_model -> the provider's configured model is preserved."""
monkeypatch.setattr(
runtime_provider_custom,
rp,
"_get_named_custom_provider",
lambda p: {
"name": "myproxy",
@@ -1760,7 +1760,7 @@ def test_custom_provider_without_target_model_keeps_default(monkeypatch):
def test_custom_provider_pool_target_model_wins(monkeypatch):
"""Pooled-credentials path also honors target_model over the default."""
monkeypatch.setattr(
runtime_provider_custom,
rp,
"_try_resolve_from_custom_pool",
lambda *a, **k: {
"provider": "custom",
@@ -1769,7 +1769,7 @@ def test_custom_provider_pool_target_model_wins(monkeypatch):
},
)
monkeypatch.setattr(
runtime_provider_custom,
rp,
"_get_named_custom_provider",
lambda p: {
"name": "myproxy",
@@ -28,7 +28,7 @@ class TestCmdSetupNonTtyGuard:
"""Non-TTY with server-url and project-id but no token → reports --access-token."""
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
monkeypatch.setattr(
"agent.secret_sources.bitwarden.find_bws", lambda install_if_missing=False: "/usr/bin/bws"
"hermes_cli.secrets_cli.bw.find_bws", lambda install_if_missing=False: "/usr/bin/bws"
)
monkeypatch.setattr(
"hermes_cli.secrets_cli._bws_version", lambda _: "2.0.0"
@@ -57,7 +57,7 @@ class TestCmdSetupNonTtyGuard:
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
monkeypatch.setenv("BWS_SERVER_URL", "https://vault.bitwarden.com")
monkeypatch.setattr(
"agent.secret_sources.bitwarden.find_bws", lambda install_if_missing=False: "/usr/bin/bws"
"hermes_cli.secrets_cli.bw.find_bws", lambda install_if_missing=False: "/usr/bin/bws"
)
monkeypatch.setattr(
"hermes_cli.secrets_cli._bws_version", lambda _: "2.0.0"
@@ -66,7 +66,7 @@ class TestCmdSetupNonTtyGuard:
monkeypatch.setattr("hermes_cli.secrets_cli.save_env_value", lambda *a: None)
monkeypatch.setattr("hermes_cli.secrets_cli.get_env_path", lambda: "/tmp/.env")
monkeypatch.setattr(
"agent.secret_sources.bitwarden.fetch_bitwarden_secrets",
"hermes_cli.secrets_cli.bw.fetch_bitwarden_secrets",
lambda **kw: ({"KEY": "val"}, []),
)
@@ -14,7 +14,6 @@ import pytest
from hermes_cli import onepassword_secrets_cli as op_cli
from hermes_cli import secrets_cli as bw_cli
from agent.secret_sources import bitwarden as bw_backend
# ---------------------------------------------------------------------------
@@ -46,7 +45,7 @@ def bw_env(monkeypatch, tmp_path):
)
monkeypatch.setattr(bw_cli, "get_env_path", lambda: tmp_path / ".env")
monkeypatch.setattr(
bw_backend, "find_bws",
bw_cli.bw, "find_bws",
lambda install_if_missing=True: Path("/fake/bws"),
)
return saved
@@ -57,7 +56,7 @@ def bw_env(monkeypatch, tmp_path):
def test_bw_token_no_verify_skips_probe(bw_env, monkeypatch):
probe = mock.Mock()
monkeypatch.setattr(bw_cli, "_list_projects", probe)
monkeypatch.setattr(bw_backend, "clear_caches", lambda *a, **kw: None)
monkeypatch.setattr(bw_cli.bw, "clear_caches", lambda *a, **kw: None)
rc = bw_cli.cmd_token(_bw_args(access_token="0.x", no_verify=True))
assert rc == 0
probe.assert_not_called()
@@ -44,7 +44,7 @@ class TestCredentialPoolPreservedOnAutoDetect:
patch('hermes_cli.model_normalize.normalize_model_for_provider', return_value='test-model'), \
patch('agent.credential_pool.load_pool', return_value=MagicMock()), \
patch('hermes_cli.config.load_config', return_value={}), \
patch('hermes_cli.config_providers.get_compatible_custom_providers', return_value=[]), \
patch('hermes_cli.config.get_compatible_custom_providers', return_value=[]), \
patch('agent.iteration_budget.IterationBudget'), \
patch('hermes_cli.config.cfg_get', return_value=None):
@@ -357,7 +357,7 @@ class TestCustomProviderOpenAIWireCapability:
pytest.fail("unrelated built-in route performed custom capability lookup")
monkeypatch.setattr(
"hermes_cli.config_providers.get_custom_provider_model_capability",
"hermes_cli.config.get_custom_provider_model_capability",
unexpected_lookup,
)
@@ -100,7 +100,7 @@ def test_credential_rotation_does_not_carry_global_headers_across_routes():
with (
patch("hermes_cli.config.load_config_readonly", return_value=config),
patch(
"hermes_cli.config_providers.get_compatible_custom_providers",
"hermes_cli.config.get_compatible_custom_providers",
return_value=config["custom_providers"],
),
):
@@ -23,7 +23,6 @@ import pytest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
from run_agent import AIAgent
from hermes_cli import runtime_provider_custom
DEFAULT_BASE = "https://api.openai.com/v1"
LOCAL_BASE = "http://127.0.0.1:39080"
@@ -238,7 +237,7 @@ def named_custom_provider(monkeypatch):
import hermes_cli.runtime_provider as rp
monkeypatch.setattr(
runtime_provider_custom,
rp,
"_get_named_custom_provider",
lambda requested: block if requested == "longcat" else None,
)
+2 -2
View File
@@ -298,8 +298,8 @@ def test_lmstudio_switch_uses_destination_context_and_verified_runtime(monkeypat
monkeypatch.setattr("hermes_cli.config.load_config", fake_load_config)
monkeypatch.setattr("hermes_cli.config.load_config_readonly", fake_load_config)
monkeypatch.setattr("hermes_cli.config_providers.get_compatible_custom_providers", fake_compatible)
monkeypatch.setattr("hermes_cli.config_providers.get_custom_provider_context_length", fake_provider_context)
monkeypatch.setattr("hermes_cli.config.get_compatible_custom_providers", fake_compatible)
monkeypatch.setattr("hermes_cli.config.get_custom_provider_context_length", fake_provider_context)
monkeypatch.setattr(AIAgent, "_ensure_lmstudio_runtime_loaded", fake_lmstudio_load)
with patch("agent.model_metadata.get_model_context_length", return_value=100_000) as mock_ctx_len:
+2 -2
View File
@@ -11527,7 +11527,7 @@ def test_plugins_list_surfaces_loader_error(monkeypatch):
def test_complete_slash_surfaces_completer_error(monkeypatch):
with patch(
"hermes_cli.commands_completion.SlashCommandCompleter",
"hermes_cli.commands.SlashCommandCompleter",
side_effect=Exception("no completer"),
):
resp = server.handle_request(
@@ -16276,7 +16276,7 @@ def test_model_options_preserves_canonical_custom_row_after_agent_init(monkeypat
)
canonical = Mock(return_value="custom:local-ollama")
monkeypatch.setattr(
"hermes_cli.runtime_provider_custom.canonical_custom_identity",
"hermes_cli.runtime_provider.canonical_custom_identity",
canonical,
)
monkeypatch.setattr(
+4 -5
View File
@@ -8,7 +8,6 @@ from tools.cronjob_tools import (
check_cronjob_requirements,
cronjob,
)
from hermes_cli import runtime_provider_custom
# =========================================================================
@@ -323,9 +322,9 @@ class TestUnifiedCronjobTool:
@staticmethod
def _patch_named_legit(monkeypatch):
import hermes_cli.runtime_provider as rp
monkeypatch.setattr(runtime_provider_custom, "has_named_custom_provider", lambda n: True)
monkeypatch.setattr(rp, "has_named_custom_provider", lambda n: True)
monkeypatch.setattr(
runtime_provider_custom, "_get_named_custom_provider",
rp, "_get_named_custom_provider",
lambda n: {"name": "legit", "base_url": "https://legit.example/v1",
"api_key": "sk-legit"},
)
@@ -659,9 +658,9 @@ class TestValidateCronBaseUrl:
@staticmethod
def _patch_named_legit(monkeypatch):
import hermes_cli.runtime_provider as rp
monkeypatch.setattr(runtime_provider_custom, "has_named_custom_provider", lambda n: True)
monkeypatch.setattr(rp, "has_named_custom_provider", lambda n: True)
monkeypatch.setattr(
runtime_provider_custom, "_get_named_custom_provider",
rp, "_get_named_custom_provider",
lambda n: {"name": "legit", "base_url": "https://legit.example/v1", "api_key": "sk-legit"},
)
-3
View File
@@ -11,7 +11,6 @@ See: https://github.com/NousResearch/hermes-agent/issues/1264
import os
import subprocess
import sys
import threading
from pathlib import Path
from unittest.mock import MagicMock, patch
@@ -49,7 +48,6 @@ def _run_with_env(extra_os_env=None, self_env=None):
"""Execute a command via LocalEnvironment with mocked Popen
and return the env dict passed to the subprocess."""
captured = {}
fake_interrupt = threading.Event()
test_environ = {
"PATH": "/usr/bin:/bin",
"HOME": "/home/user",
@@ -62,7 +60,6 @@ def _run_with_env(extra_os_env=None, self_env=None):
with patch("tools.environments.local._find_bash", return_value="/bin/bash"), \
patch("subprocess.Popen", side_effect=_make_fake_popen(captured)), \
patch("tools.interrupt._interrupt_event", fake_interrupt), \
patch.dict(os.environ, test_environ, clear=True):
env.execute("echo hello")

Some files were not shown because too many files have changed in this diff Show More